Skip to content

Source Code Reference for the CLI.

cli

Modules:

Name Description
cli

Photovoltaic electricity generation potential for different technologies & configurations

data_model
irradiance
manual
meteo
performance
plot
position
power
print
rich_help_panel_names

Structural components of the command line interface

series
surface
time

Important sun and solar surface geometry parameters in calculating the amount of solar radiation that reaches a particular location on the Earth's surface

write

cli

Photovoltaic electricity generation potential for different technologies & configurations

Functions:

Name Description
main

The main entry point for PVIS prototype

main

main(
    version: Annotated[bool, typer_option_version] = None,
    verbose: Annotated[int, typer_option_verbose] = 0,
    log: Annotated[int | None, typer_option_log] = None,
    log_rich_handler: Annotated[
        bool, typer_option_log_rich_handler
    ] = False,
    log_file: Annotated[
        Path | None, typer_option_logfile
    ] = None,
) -> None

The main entry point for PVIS prototype

Source code in pvgisprototype/cli/cli.py
@app.callback(no_args_is_help=True)
def main(
    version: Annotated[bool, typer_option_version] = None,
    verbose: Annotated[int, typer_option_verbose] = 0,
    log: Annotated[int | None, typer_option_log] = None,
    log_rich_handler: Annotated[bool, typer_option_log_rich_handler] = False,
    log_file: Annotated[Path | None, typer_option_logfile] = None,
) -> None:
    """
    The main entry point for PVIS prototype
    """
    if verbose:
        print("Will write verbose output")
        state["verbose"] = True

data_model

Modules:

Name Description
analyse
data_model
inspect
visualise

analyse

Functions:

Name Description
analyse_centrality

Identify critical nodes (models) that act as hubs or bridges

analyse_dependency_structure

A long critical path suggests a deep dependency chain, which may impact

analyse_graph

Calculate fundamental metrics to understand the scale and density of

analyse_path_length
detect_cycles_and_strongly_connected_components
  • Cycles (e.g., A → B → A) indicate potential design flaws.
detect_densely_connected_components

Detect clusters of densely connected models to assess modularity

analyse_centrality

analyse_centrality(source_path: Path) -> None

Identify critical nodes (models) that act as hubs or bridges - High in-degree nodes are critical for many models. - High betweenness nodes act as bridges between modules.

Source code in pvgisprototype/cli/data_model/analyse.py
def analyse_centrality(
    source_path: Path,
) -> None:
    """
    Identify critical nodes (models) that act as hubs or bridges
    - High in-degree nodes are critical for many models.
    - High betweenness nodes act as bridges between modules.

    """
    graph = build_dependency_graph(source_path=source_path)
    in_degrees = dict(graph.in_degree())
    out_degrees = dict(graph.out_degree())
    betweenness = nx.betweenness_centrality(graph)
    pagerank = nx.pagerank(graph)

    print("Top 5 nodes by in-degree (most dependencies) :")
    for node in sorted(in_degrees, key=in_degrees.get, reverse=True)[:5]:
        print(f"{node}: {in_degrees[node]}")

    print("\nTop 5 nodes by out-degree (most ) :")
    for node in sorted(out_degrees, key=out_degrees.get, reverse=True)[:5]:
        print(f"{node}: {out_degrees[node]}")

    print("\nTop 5 nodes by betweenness centrality (bridges) :")
    for node in sorted(betweenness, key=betweenness.get, reverse=True)[:5]:
        print(f"{node}: {betweenness[node]:.4f}")

    print("\nTop 5 nodes by PageRank (influence) :")
    for node in sorted(pagerank, key=pagerank.get, reverse=True)[:5]:
        print(f"{node}: {pagerank[node]:.4f}")

analyse_dependency_structure

analyse_dependency_structure(source_path: Path) -> None

A long critical path suggests a deep dependency chain, which may impact performance or testing complexity.

Source code in pvgisprototype/cli/data_model/analyse.py
def analyse_dependency_structure(
    source_path: Path,
) -> None:
    """
    A long critical path suggests a deep dependency chain, which may impact
    performance or testing complexity.

    """
    graph = build_dependency_graph(source_path=source_path)
    if nx.is_directed_acyclic_graph(G=graph):
        topological_order = list(nx.topological_sort(G=graph))
        print("Topological order (first 5):", topological_order[:5])

        # Longest path (critical path)
        longest_path = nx.dag_longest_path(G=graph)
        print(f"Critical path length: {len(longest_path)}")
        print(f"Critical path: {longest_path}")

    else:
        print("Graph is not a DAG. Skipping topological analysis.")

analyse_graph

analyse_graph(source_path: Path) -> None

Calculate fundamental metrics to understand the scale and density of dependencies. - High node/edge counts suggest complexity. - High density (many edges relative to nodes) indicates tightly interconnected models, which may complicate maintenance.

Source code in pvgisprototype/cli/data_model/analyse.py
def analyse_graph(
    source_path: Path,
) -> None:
    """
    Calculate fundamental metrics to understand the scale and density of
    dependencies.
    - High node/edge counts suggest complexity.
    - High density (many edges relative to nodes) indicates tightly
      interconnected models, which may complicate maintenance.

    """
    graph = build_dependency_graph(source_path=source_path)
    print(f"Number of nodes (models): {graph.number_of_nodes()}")
    print(f"Number of edges (dependencies): {graph.number_of_edges()}")
    print(f"Graph density: {nx.density(G=graph):.4f}")
    print(
        f"Is the graph a DAG (Directed Acyclic Graph)? {nx.is_directed_acyclic_graph(G=graph)}"
    )

analyse_path_length

analyse_path_length(source_path: Path) -> None
Source code in pvgisprototype/cli/data_model/analyse.py
def analyse_path_length(
    source_path: Path,
) -> None:
    """
    Interpretation:
        A low average path length suggests a "small-world" structure, where
        models are reachable in few steps.

    """
    graph = build_dependency_graph(source_path=source_path)

    if nx.is_strongly_connected(G=graph):
        avg_path = nx.average_shortest_path_length(G=graph)
        print(f"Average shortest path length: {avg_path:.2f}")

    else:
        print("Graph is not strongly connected. Calculating for largest component...")
        largest_cc = max(nx.strongly_connected_components(G=graph), key=len)
        G_sub = graph.subgraph(largest_cc)
        avg_path = nx.average_shortest_path_length(G=G_sub)
        print(f"Average shortest path in largest SCC: {avg_path:.2f}")

detect_cycles_and_strongly_connected_components

detect_cycles_and_strongly_connected_components(
    source_path: Path,
) -> None
  • Cycles (e.g., A → B → A) indicate potential design flaws.
  • Large Strongly Connected Components suggest tightly interdependent modules needing refactoring.
Source code in pvgisprototype/cli/data_model/analyse.py
def detect_cycles_and_strongly_connected_components(
    source_path: Path,
) -> None:
    """
    - Cycles (e.g., A → B → A) indicate potential design flaws.
    - Large Strongly Connected Components suggest tightly interdependent
      modules needing refactoring.

    """
    graph = build_dependency_graph(source_path=source_path)
    try:
        cycles = list(nx.simple_cycles(G=graph))
        print(f"Found {len(cycles)} cycles. Examples:")
        for cycle in cycles[:3]:  # Show first 3
            print(" → ".join(cycle))

    except nx.NetworkXNoCycle:
        print("No cycles detected.")

    strongly_connected_components = list(nx.strongly_connected_components(G=graph))

    print("Strongly Connected Components")
    print(f"Number of components : {len(strongly_connected_components)}")
    print(
        "Largest component :",
        (
            max(strongly_connected_components, key=len)
            if strongly_connected_components
            else "N/A"
        ),
    )

detect_densely_connected_components

detect_densely_connected_components(
    source_path: Path,
) -> None

Detect clusters of densely connected models to assess modularity

Interpretation: - Communities may align with functional domains (e.g., solar position, energy yield, temperature). - High modularity suggests well-organized, maintainable code.

Source code in pvgisprototype/cli/data_model/analyse.py
def detect_densely_connected_components(
    source_path: Path,
) -> None:
    """
    Detect clusters of densely connected models to assess modularity


    Interpretation:
    - Communities may align with functional domains (e.g., solar position,
      energy yield, temperature).
    - High modularity suggests well-organized, maintainable code.

    """
    graph = build_dependency_graph(source_path=source_path)
    G_undirected = graph.to_undirected()
    communities = nx_comm.greedy_modularity_communities(G=G_undirected)
    print(f"Detected {len(communities)} communities.")
    for i, community in enumerate(communities):
        print(f"Community {i+1} (size {len(community)}): {community}")

data_model

Functions:

Name Description
main

Inspect data model definitions including YAML files, Python dictionaries

main

main(
    ctx: Context,
    verbose: Annotated[bool, Option(help=Verbose)] = False,
    log_file: Annotated[
        str | None,
        Option(--log - file, -l, help="Log file"),
    ] = LOG_FILE,
    log_level: str = LOG_LEVEL,
    rich_handler: Annotated[
        bool,
        Option(--rich, --no - rich, help="Rich handler"),
    ] = RICH_HANDLER,
)

Inspect data model definitions including YAML files, Python dictionaries and native PVGIS data models.

Source code in pvgisprototype/cli/data_model/data_model.py
@app.callback()
def main(
    ctx: typer.Context,
    verbose: Annotated[bool, typer.Option(help="Verbose")] = False,
    log_file: Annotated[
        str | None, typer.Option("--log-file", "-l", help="Log file")
    ] = LOG_FILE,
    log_level: str = LOG_LEVEL,
    rich_handler: Annotated[
        bool, typer.Option("--rich", "--no-rich", help="Rich handler")
    ] = RICH_HANDLER,
):
    """
    Inspect data model definitions including YAML files, Python dictionaries
    and native PVGIS data models.
    """
    if verbose:
        log_level = "DEBUG"
    setup_factory_logger(level=log_level, file=log_file, rich_handler=rich_handler)

    # Store logging config in context for child commands
    if not ctx.obj:
        ctx.obj = {}
    ctx.obj["logger_configuration"] = {
        "log_level": log_level,
        "log_file": log_file,
        "rich_handler": rich_handler,
    }

inspect

Functions:

Name Description
inspect_pvgis_data_model
inspect_python_definition

Inspect a specific dictionary key from a Python module.

inspect_yaml_definition
load_python_module

Dynamically load a Python module from a file path.

inspect_pvgis_data_model

inspect_pvgis_data_model(data_model: str = 'all')
Source code in pvgisprototype/cli/data_model/inspect.py
def inspect_pvgis_data_model(
    data_model: str = 'all',
):
    """
    """
    pass

inspect_python_definition

inspect_python_definition(
    python_module: Path,
    definition: str = "Fingerprint",
    attribute: str | None = None,
    verbose: Annotated[bool, Option(help=Verbose)] = False,
    log_file: Annotated[
        str | None,
        Option(--log - file, -l, help="Log file"),
    ] = LOG_FILE,
    log_level: str = LOG_LEVEL,
    rich_handler: Annotated[
        bool,
        Option(--rich, --no - rich, help="Rich handler"),
    ] = RICH_HANDLER,
)

Inspect a specific dictionary key from a Python module.

Source code in pvgisprototype/cli/data_model/inspect.py
def inspect_python_definition(
    python_module: Path,
    definition: str = "Fingerprint",  # The key to look for in the module's dictionary
    attribute: str | None = None,
    verbose: Annotated[bool, typer.Option(help="Verbose")] = False,
    log_file: Annotated[
        str | None, typer.Option("--log-file", "-l", help="Log file")
    ] = LOG_FILE,
    log_level: str = LOG_LEVEL,
    rich_handler: Annotated[
        bool, typer.Option("--rich", "--no-rich", help="Rich handler")
    ] = RICH_HANDLER,
):
    """Inspect a specific dictionary key from a Python module."""

    # Initialize logging
    setup_factory_logger(
        verbose=verbose,
        level=log_level,
        file=log_file,
        rich_handler=rich_handler,
    )

    if not definition:
        print(f"List all top-level data model definition names")

    else:
        try:
            # Load the data model definitions Python dictionary
            module = load_python_module(python_module)
            definitions = module.PVGIS_DATA_MODEL_DEFINITIONS

            # Extract the specific data model
            if definition in definitions:
                output = definitions[definition]

                if attribute and attribute in output:
                    output = definitions[definition][attribute]

                # Print in YAML format for consistent visualization
                print(
                    yaml.dump(
                        data=output,
                        sort_keys=False,
                        default_flow_style=False,
                        indent=2
                    )
                )
            else:
                logger.warning(f"Key '{definition_key}' not found in the definitions dictionary.")
                raise typer.Exit(code=1)

        except AttributeError:
            typer.echo("The module does not have a 'PVGIS_DATA_MODEL_DEFINITIONS' variable.")
            raise typer.Exit(code=1)

        except Exception as e:
            typer.echo(f"Error: {str(e)}")
            raise typer.Exit(code=1)

inspect_yaml_definition

inspect_yaml_definition(
    yaml_file: Path,
    verbose: Annotated[bool, Option(help=Verbose)] = False,
    log_file: Annotated[
        str | None,
        Option(--log - file, -l, help="Log file"),
    ] = LOG_FILE,
    log_level: str = LOG_LEVEL,
    rich_handler: Annotated[
        bool,
        Option(--rich, --no - rich, help="Rich handler"),
    ] = RICH_HANDLER,
)
Source code in pvgisprototype/cli/data_model/inspect.py
def inspect_yaml_definition(
    yaml_file: Path,
    verbose: Annotated[bool, typer.Option(help="Verbose")] = False,
    log_file: Annotated[
        str | None, typer.Option("--log-file", "-l", help="Log file")
    ] = LOG_FILE,
    log_level: str = LOG_LEVEL,
    rich_handler: Annotated[
        bool, typer.Option("--rich", "--no-rich", help="Rich handler")
    ] = RICH_HANDLER,
):
    """ """
    # Initialize logging
    setup_factory_logger(
        verbose=verbose,
        level=log_level,
        file=log_file,
        rich_handler=rich_handler,
    )

    print(
            yaml.dump(
                load_yaml_file(yaml_file),
                sort_keys=False,
            )
        )

load_python_module

load_python_module(module_path: Path) -> Any

Dynamically load a Python module from a file path.

Source code in pvgisprototype/cli/data_model/inspect.py
def load_python_module(module_path: Path) -> Any:
    """Dynamically load a Python module from a file path."""
    module_name = module_path.stem
    spec = util.spec_from_file_location(module_name, module_path)
    if spec is None:
        raise ImportError(f"Could not load module from {module_path}")

    module = util.module_from_spec(spec)
    spec.loader.exec_module(module)

    return module

visualise

Functions:

Name Description
visualise_circular_tree

Prototype for the command line

visualise_graph

Prototype for the command line

visualise_gravis_d3

Prototype for the command line

visualise_hierarchical_graph

Prototype for the command line

visualise_circular_tree

visualise_circular_tree(
    source_path: Annotated[
        Path,
        Option(
            help="Source directory with YAML data model descriptions"
        ),
    ] = Path("output/complex_example"),
    node_size: Annotated[
        int, Option(help="Node size")
    ] = 20,
) -> None

Prototype for the command line

Source code in pvgisprototype/cli/data_model/visualise.py
def visualise_circular_tree(
    source_path: Annotated[
        Path, typer.Option(help="Source directory with YAML data model descriptions")
    ] = Path(
        "output/complex_example"
    ),  # definitions.yaml"),
    node_size: Annotated[int, typer.Option(help="Node size")] = 20,
) -> None:
    """
    Prototype for the command line
    """
    generate_circular_tree(
        source_path=source_path,
        node_size=node_size,
    )

visualise_graph

visualise_graph(
    source_path: Annotated[
        Path,
        Option(
            help="Source directory with YAML data model descriptions"
        ),
    ] = Path("output/complex_example"),
    node_size: Annotated[
        int, Option(help="Node size")
    ] = 2400,
    parent_node_size: Annotated[
        int, Option(help="Parent node size")
    ] = 800,
) -> None

Prototype for the command line

Source code in pvgisprototype/cli/data_model/visualise.py
def visualise_graph(
    source_path: Annotated[
        Path, typer.Option(help="Source directory with YAML data model descriptions")
    ] = Path(
        "output/complex_example"
    ),  # definitions.yaml"),
    # yaml_file: Path = Path("output/complex_example/complex.yaml"),
    node_size: Annotated[int, typer.Option(help="Node size")] = 2400,
    parent_node_size: Annotated[int, typer.Option(help="Parent node size")] = 800,
) -> None:
    """
    Prototype for the command line
    """
    generate_graph(
        source_path=source_path,
        # yaml_file=yaml_file,
        node_size=node_size,
        parent_node_size=parent_node_size,
    )

visualise_gravis_d3

visualise_gravis_d3(
    ctx: Context,
    yaml_file: Annotated[
        Path,
        Option(help="A YAML PVGIS data model description"),
    ] = Path("definitions.yaml"),
    output_file: Path = Path("data_model_graph.html"),
) -> None

Prototype for the command line

Source code in pvgisprototype/cli/data_model/visualise.py
def visualise_gravis_d3(
    ctx: typer.Context,  # Access parent context
    yaml_file: Annotated[
        Path, typer.Option(help="A YAML PVGIS data model description")
    ] = Path(
        "definitions.yaml"
    ),  # definitions.yaml"),
    # yaml_file: Path = Path("definitions.yaml/data_model_template.yaml"),
    output_file: Path = Path("data_model_graph.html"),
    # node_size: Annotated[int, typer.Option(help="Node size")] = 2400,
    # parent_node_size: Annotated[int, typer.Option(help="Parent node size")] = 800,
) -> None:
    """
    Prototype for the command line
    """
    # Get logging config from parent context if available
    log_config = getattr(ctx.obj, 'log_config', {}) if ctx and ctx.obj else {}
    generate_gravis_d3(
        yaml_file=yaml_file,
        # yaml_file=yaml_file,
        output_file=output_file,
        # node_size=node_size,
        # parent_node_size=parent_node_size,
        **log_config
    )

visualise_hierarchical_graph

visualise_hierarchical_graph(
    source_path: Annotated[
        Path,
        Option(
            help="Source directory with YAML data model descriptions"
        ),
    ] = Path("output/complex_example"),
) -> None

Prototype for the command line

Source code in pvgisprototype/cli/data_model/visualise.py
def visualise_hierarchical_graph(
    source_path: Annotated[
        Path, typer.Option(help="Source directory with YAML data model descriptions")
    ] = Path(
        "output/complex_example"
    ),  # definitions.yaml"),
) -> None:
    """
    Prototype for the command line
    """
    generate_hierarchical_graph(source_path)

irradiance

Modules:

Name Description
introduction
kato_bands
limits
reflectivity

introduction

Functions:

Name Description
solar_irradiance_introduction

A short introduction on solar irradiance

solar_irradiance_introduction

solar_irradiance_introduction()

A short introduction on solar irradiance

Source code in pvgisprototype/cli/irradiance/introduction.py
def solar_irradiance_introduction():
    """A short introduction on solar irradiance"""
    introduction = """
    [underline]Solar irradiance[/underline] is ...
    """
    note = """
    PVGIS can model solar irradiance components or read selectively
    [magenta]global[/magenta] or [magenta]direct[/magenta] irradiance time series from external datasets.
    """
    from rich.panel import Panel

    note_in_a_panel = Panel(
        "[italic]{}[/italic]".format(note),
        title="[bold cyan]Note[/bold cyan]",
        width=78,
    )
    from rich.console import Console

    console = Console()
    # introduction.wrap(console, 30)
    console.print(introduction)
    console.print(note_in_a_panel)
    console.print(A_PRIMER_ON_SOLAR_IRRADIANCE)

kato_bands

Functions:

Name Description
print_kato_spectral_bands

print_kato_spectral_bands

print_kato_spectral_bands()
Source code in pvgisprototype/cli/irradiance/kato_bands.py
def print_kato_spectral_bands():
    """
    """
    print(KATO_BANDS)

limits

Functions:

Name Description
calculate_physical_limits

Calculate physically possible limits.

calculate_rare_limits

Calculate extremely rare limits.

print_limits_table

Print table of physically possible irradiance limits

calculate_physical_limits

calculate_physical_limits(
    solar_zenith: float,
    air_temperature: float = 300,
    rounding_places: int = 5,
)

Calculate physically possible limits.

Source code in pvgisprototype/cli/irradiance/limits.py
@app.command(
    "physical",
    no_args_is_help=True,
    rich_help_panel=rich_help_panel_irradiance_series,
)
def calculate_physical_limits(
    solar_zenith: float,
    air_temperature: float = 300,
    rounding_places: int = 5,
):
    """Calculate physically possible limits."""
    limits = calculate_limits(solar_zenith, air_temperature, PHYSICALLY_POSSIBLE_LIMITS)
    print_limits_table(limits_dictionary=limits, rounding_places=rounding_places)
    return limits

calculate_rare_limits

calculate_rare_limits(
    solar_zenith: float,
    air_temperature: float = 300,
    rounding_places: int = 5,
)

Calculate extremely rare limits.

Source code in pvgisprototype/cli/irradiance/limits.py
@app.command(
    "rare",
    no_args_is_help=True,
    rich_help_panel=rich_help_panel_irradiance_series,
)
def calculate_rare_limits(
    solar_zenith: float,
    air_temperature: float = 300,
    rounding_places: int = 5,
):
    """Calculate extremely rare limits."""
    limits = calculate_limits(solar_zenith, air_temperature, EXTREMELY_RARE_LIMITS)
    print_limits_table(limits_dictionary=limits, rounding_places=rounding_places)
    return limits

print_limits_table

print_limits_table(
    limits_dictionary,
    rounding_places=ROUNDING_PLACES_DEFAULT,
)

Print table of physically possible irradiance limits

Source code in pvgisprototype/cli/irradiance/limits.py
def print_limits_table(
    limits_dictionary,
    rounding_places=ROUNDING_PLACES_DEFAULT,
):
    """Print table of physically possible irradiance limits"""
    limits_dictionary = round_float_values(limits_dictionary, rounding_places)
    table = Table(box=box.SIMPLE_HEAD)
    table.add_column("Component")
    table.add_column("Min", justify="right")
    table.add_column("Max", justify="right")

    for component, limits in limits_dictionary.items():
        table.add_row(component, str(limits["Min"]), str(limits["Max"]))

    Console().print(table)

reflectivity

Functions:

Name Description
get_reflectivity_factor_for_direct_irradiance_series

Notes

get_reflectivity_factor_for_nondirect_irradiance

get_reflectivity_factor_for_direct_irradiance_series

get_reflectivity_factor_for_direct_irradiance_series(
    solar_incidence_series: Annotated[
        List[float], typer_argument_solar_incidence_series
    ],
    angular_loss_coefficient: float = ANGULAR_LOSS_COEFFICIENT,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
)
Notes

This CLI function uses an implementation of the solar incidence angle modifier as per Martin & Ruiz (2005). Expected is the angle between the sun-solar-surface vector and the vector normal to the reference solar surface. We call this the typical incidence angle as opposed to the complementary incidence angle defined by Jenčo (1992).

Source code in pvgisprototype/cli/irradiance/reflectivity.py
@app.command(
    "direct",
    no_args_is_help=True,
    help=f"⦟ Solar incidence angle modifier for direct inclined irradiance due to reflectivity by Martin & Ruiz, 2005 {NOT_COMPLETE_CLI}",
    short_help=f"⦟ Solar incidence angle modifier for direct irradiance due to reflectivity {NOT_COMPLETE_CLI}",
    rich_help_panel=rich_help_panel_toolbox,
)
def get_reflectivity_factor_for_direct_irradiance_series(
    solar_incidence_series: Annotated[
        List[float], typer_argument_solar_incidence_series
    ],
    angular_loss_coefficient: float = ANGULAR_LOSS_COEFFICIENT,
    # csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    # dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    # array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    # uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    # resample_large_series: Annotated[bool, 'Resample large time series?'] = False,
    # terminal_width_fraction: Annotated[float, typer_option_uniplot_terminal_width] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    # fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    # metadata: Annotated[bool, typer_option_command_metadata] = False,
    # panels: Annotated[bool, typer_option_panels_output] = False,
    # index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
):
    """
    Notes
    -----
    This CLI function uses an implementation of the solar incidence angle
    modifier as per Martin & Ruiz (2005). Expected is the angle between the
    sun-solar-surface vector and the vector normal to the reference solar
    surface. We call this the _typical_ incidence angle as opposed to the
    _complementary_ incidence angle defined by Jenčo (1992).

    """
    reflectivity_factor_for_direct_irradiance_series = (
        calculate_reflectivity_factor_for_direct_irradiance_series(
            solar_incidence_series=solar_incidence_series,
            angular_loss_coefficient=angular_loss_coefficient,
            verbose=verbose,
            log=log,
        )
    )
    if not quiet:
        if verbose > 0:
            pass
        else:
            flat_list = (
                reflectivity_factor_for_direct_irradiance_series.flatten().astype(str)
            )
            csv_str = ",".join(flat_list)
            print(csv_str)

get_reflectivity_factor_for_nondirect_irradiance

get_reflectivity_factor_for_nondirect_irradiance(
    indirect_angular_loss_coefficient: float,
    angular_loss_coefficient: float = ANGULAR_LOSS_COEFFICIENT,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
)
Source code in pvgisprototype/cli/irradiance/reflectivity.py
@app.command(
    "indirect",
    no_args_is_help=True,
    help=f"⦟ Solar incidence angle modifier for non-direct inclined irradiance due to reflectivity by Martin & Ruiz, 2005 {NOT_COMPLETE_CLI}",
    short_help=f"⦟ Solar incidence angle modifier for non-direct irradiance due to reflectivity {NOT_COMPLETE_CLI}",
    rich_help_panel=rich_help_panel_toolbox,
)
def get_reflectivity_factor_for_nondirect_irradiance(
    indirect_angular_loss_coefficient: float,
    angular_loss_coefficient: float = ANGULAR_LOSS_COEFFICIENT,
    # csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    # dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    # array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    # uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    # resample_large_series: Annotated[bool, 'Resample large time series?'] = False,
    # terminal_width_fraction: Annotated[float, typer_option_uniplot_terminal_width] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    # fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    # metadata: Annotated[bool, typer_option_command_metadata] = False,
    # panels: Annotated[bool, typer_option_panels_output] = False,
    # index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
):
    """ """
    reflectivity_factor_for_nondirect_irradiance = (
        calculate_reflectivity_factor_for_nondirect_irradiance(
            indirect_angular_loss_coefficient=indirect_angular_loss_coefficient,
            angular_loss_coefficient=angular_loss_coefficient,
            verbose=verbose,
        )
    )
    if not quiet:
        if verbose > 0:
            pass
        else:
            print(reflectivity_factor_for_nondirect_irradiance)

manual

Functions:

Name Description
update_solar_radiation_dictionary

Examples

update_solar_radiation_dictionary

update_solar_radiation_dictionary(
    lemma: Annotated[str, Argument(help=Lemma)],
    lemma_name: Annotated[
        str, Argument(help=lemma_name(snake_case))
    ],
    symbol: Annotated[
        str, Argument(help="Symbol for Lemma")
    ],
    description: Annotated[
        str, Argument(help="Description of Lemma")
    ],
    units: Annotated[str, Argument(help="Units for Lemma")],
    tags: Annotated[
        str,
        Argument(help="Tags for Lemma (comma-separated)"),
    ],
    dictionary_yaml_file: Annotated[
        Path,
        Option(help="Path to the dictionary YAML file"),
    ] = Path(".data/solar_radiation_dictionary.yml"),
)

Examples:

pvis manual update "lemma-name" lemma_name "Symbol" "Description" "Units" "tag1, tag2, tag3"

Source code in pvgisprototype/cli/manual.py
@app.command("update", no_args_is_help=True, help="Update")
def update_solar_radiation_dictionary(
    lemma: Annotated[str, typer.Argument(help="Lemma")],
    lemma_name: Annotated[str, typer.Argument(help="lemma_name (snake_case)")],
    symbol: Annotated[str, typer.Argument(help="Symbol for Lemma")],
    description: Annotated[str, typer.Argument(help="Description of Lemma")],
    units: Annotated[str, typer.Argument(help="Units for Lemma")],
    tags: Annotated[str, typer.Argument(help="Tags for Lemma (comma-separated)")],
    dictionary_yaml_file: Annotated[
        Path, typer.Option(help="Path to the dictionary YAML file")
    ] = Path(".data/solar_radiation_dictionary.yml"),
):
    """
    Examples
    --------
    pvis manual update "lemma-name" lemma_name "Symbol" "Description" "Units" "tag1, tag2, tag3"
    """
    tags_list = [tag.strip() for tag in tags.split(",")]

    new_entry = {
        "lemma": lemma,
        "lemma_name": lemma_name,
        "symbol": symbol,
        "description": description,
        "units": units,
        "tags": tags_list,
    }

    with open(dictionary_yaml_file, "r") as file:
        data = yaml.safe_load(file)

    existing_lemma = next((v for v in data if v["lemma_name"] == lemma_name), None)

    if existing_lemma:
        existing_lemma.update(new_entry)
        typer.echo(f"Updated existing lemma: {lemma_name}")
    else:
        data.append(new_entry)
        typer.echo(f"Added new lemma: {lemma_name}")

    with open(dictionary_yaml_file, "w") as file:
        yaml.dump(data, file)

meteo

Modules:

Name Description
introduction
meteo
tmy

introduction

Functions:

Name Description
introduction

A short introduction on the Typical Meteorological Year

introduction

introduction()

A short introduction on the Typical Meteorological Year

Source code in pvgisprototype/cli/meteo/introduction.py
def introduction():
    """
    A short introduction on the Typical Meteorological Year
    """
    introduction = """The [underline]Typical Meteorological Year[/underline]
    (TMY) is a dataset designed to represent the most _typical_ weather
    conditions for each month at a given location, using historical data. This
    dataset is particularly useful for simulations in solar energy and building
    performance."""
    note = """Internally, [bold]timestamps[/bold] are converted to
    [magenta]UTC[/magenta] and [bold]angles[/bold] are measured in
    [magenta]radians[/magenta] !
    """
    from rich.panel import Panel

    note_in_a_panel = Panel(
        "[italic]{}[/italic]".format(note),
        title="[bold cyan]Note[/bold cyan]",
        width=78,
    )
    from rich.console import Console

    console = Console()
    # introduction.wrap(console, 30)
    console.print(introduction)
    console.print(note_in_a_panel)
    console.print(A_PRIMER_ON_TYPICAL_METEOROLOGICAL_YEAR)

meteo

Functions:

Name Description
main

Typical Meteorological Year

main

main(
    ctx: Context,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    debug: Annotated[
        bool, Option(--debug, help="Enable debug mode")
    ] = False,
)

Typical Meteorological Year

Source code in pvgisprototype/cli/meteo/meteo.py
@app.callback()
def main(
    ctx: typer.Context,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    debug: Annotated[bool, typer.Option(
        "--debug",
        help="Enable debug mode")] = False,
):
    """
    Typical Meteorological Year
    """
    # if verbose > 2:
    #     print(f"Executing command: {ctx.invoked_subcommand}")
    if verbose > 0:
        print("Will output verbosely")
        # state["verbose"] = True

    app.debug_mode = debug

tmy

Functions:

Name Description
calculate_degree_days

Calculate the cooling/heating degree days

tmy

Generate the Typical Meteorological Year (TMY)

tmy_weighting

Print the available weightings for a meteorological variable or full scheme.

calculate_degree_days

calculate_degree_days(
    location: Annotated[
        Tuple[float, float],
        Argument(..., help="Latitude, longitude [°]"),
    ],
    years: Annotated[
        Tuple[float, float],
        Argument(
            ...,
            min=2005,
            max=2020,
            help="First and last year of calculations",
        ),
    ],
    meteo: Annotated[
        Path,
        Argument(
            ...,
            exists=True,
            file_okay=True,
            dir_okay=False,
            writable=False,
            readable=True,
            resolve_path=True,
            help="Directory containing the meteorological data",
        ),
    ],
    elevation: Annotated[
        Path,
        Argument(
            ...,
            exists=True,
            file_okay=True,
            dir_okay=False,
            writable=False,
            readable=True,
            resolve_path=True,
            help="Directory containing the digital elevation data",
        ),
    ],
    monthly: bool = Option(
        False, is_flag=True, help="Print monthly averages"
    ),
)

Calculate the cooling/heating degree days

Args:

Returns: None

Raises: AssertionError: If the command exit code or output doesn't match the expected values.

Example:

Notes: - Refactored from the original C program degreedays_hourly as follows:

  • New flag, Old flag, Type, Old Variable, Description

  • monthly , r , Flag , - , Print monthly averages

  • meteo , g , str , dailyPrefix , Directory containing the meteo files

  • elevation , f , str , elevationFilename , Directory prefix of the DEM files

  • location , d , float, float , latitude, longitude , Latitude, longitude [°]

  • years , y , float, float , yearStart, yearEnd , First year, last year of calculations

Source code in pvgisprototype/cli/meteo/tmy.py
def calculate_degree_days(
    location: Annotated[
        Tuple[float, float], typer.Argument(..., help="Latitude, longitude [°]")
    ],
    years: Annotated[
        Tuple[float, float],
        typer.Argument(
            ..., min=2005, max=2020, help="First and last year of calculations"
        ),
    ],
    meteo: Annotated[
        Path,
        typer.Argument(
            ...,
            exists=True,
            file_okay=True,
            dir_okay=False,
            writable=False,
            readable=True,
            resolve_path=True,
            help="Directory containing the meteorological data",
        ),
    ],
    elevation: Annotated[
        Path,
        typer.Argument(
            ...,
            exists=True,
            file_okay=True,
            dir_okay=False,
            writable=False,
            readable=True,
            resolve_path=True,
            help="Directory containing the digital elevation data",
        ),
    ],
    monthly: bool = typer.Option(False, is_flag=True, help="Print monthly averages"),
):
    """Calculate the cooling/heating degree days

    Args:

    Returns:
        None

    Raises:
        AssertionError: If the command exit code or output doesn't match the expected values.

    Example:

    Notes:
    - Refactored from the original C program `degreedays_hourly` as follows:\n
      - New flag, Old flag, Type, Old Variable, Description\n
      - monthly   , r    , Flag         , -                   , Print monthly averages \n
      - meteo     , g    , str          , dailyPrefix         , Directory containing the meteo files\n
      - elevation , f    , str          , elevationFilename   , Directory prefix of the DEM files\n
      - location  , d    , float, float , latitude, longitude , Latitude, longitude [°]\n
      - years     , y    , float, float , yearStart, yearEnd  , First year, last year of calculations
    """
    pass

tmy

tmy(
    longitude: Annotated[
        float, typer_argument_longitude_in_degrees
    ] = float(),
    latitude: Annotated[
        float, typer_argument_latitude_in_degrees
    ] = float(),
    timestamps: Annotated[
        DatetimeIndex, typer_argument_naive_timestamps
    ] = str(now_datetime()),
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = None,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    convert_longitude_360: Annotated[
        bool, typer_option_convert_longitude_360
    ] = False,
    variable: Annotated[
        str | None, typer_option_data_variable
    ] = None,
    meteorological_variable: Annotated[
        List[MeteorologicalVariable],
        Option(
            help="Standard name of meteorological variable for Finkelstein-Schafer statistics"
        ),
    ] = [all],
    global_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_global_horizontal_irradiance,
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_direct_horizontal_irradiance,
    ] = None,
    temperature_series: Annotated[
        TemperatureSeries,
        typer_option_temperature_series_for_tmy,
    ] = average_air_temperature,
    relative_humidity_series: Annotated[
        RelativeHumiditySeries,
        typer_option_relative_humidity_series_for_tmy,
    ] = average_relative_humidity,
    wind_speed_series: Annotated[
        WindSpeedSeries,
        typer_option_wind_speed_series_for_tmy,
    ] = average_wind_speed,
    solar_position_model: SolarPositionModel = SOLAR_POSITION_ALGORITHM_DEFAULT,
    eccentricity_phase_offset: float = value,
    eccentricity_amplitude: float = value,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = nearest,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    output_filename: Annotated[
        Path, typer_option_output_filename
    ] = "series_in",
    variable_name_as_suffix: Annotated[
        bool, typer_option_variable_name_as_suffix
    ] = True,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = RADIANS,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    weighting_scheme: TypicalMeteorologicalMonthWeightingScheme = ISO_15927_4.value,
    plot_statistic: Annotated[
        list[TMYStatisticModel],
        Option(
            help="Select which Finkelstein-Schafer statistics to plot"
        ),
    ] = None,
    limit_x_axis_to_tmy_extent: Annotated[
        bool,
        "Limit plot of input time series to temporal extent of TMY",
    ] = True,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = NoneValue,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
)

Generate the Typical Meteorological Year (TMY)

A typical meteorological year (TMY) is a set of meteorological data with data values for every hour in a year for a given geographical location. The data are selected from hourly data in a longer time period (normally 10 years or more).

An overview of the algorithm for calculating the TMY :

  1. Input data : Read at least 10 years of hourly time series over a location

  2. Daily Statistics : Compute daily maximum, minimum, and mean for selected variables.

  3. Cumulative Distribution Function (CDF) : Compute the CDF of each variable for each month :

3.1 one CDF for each variable, month, and year. For example, for the Global Horizontal Irradiance (GHI) : one for January 2011, one for January 2012 and so on, and for each month the same for the ambient Temperature or other variables.

3.2 one long-term CDF for each variable across all years for each month.

  1. Finkelstein-Schafer Statistic :

4.1 Compute the absolute difference between the long-term CDF and the candidate month's CDF. 4.2 Compute the weighted sum (WS) of these differences for each month and year.

  1. Typical Months : Rank months by the lowest WS for each month (e.g., rank all Januaries).

  2. Month Selection :

  3. ISO method: Select months based on wind speed similarity to the long-term average.

  4. Sandia/NREL methods : Re-rank top months by their closeness to long-term averages, filtering based on extreme values.

  5. TMY : Combine the selected months into a continuous year, smoothing variable transitions at month boundaries.

Notes

Pay attention to the default return modus of this function ! Without any --verbose asked, is to print all TMY variable values as one CSV string. In the case of a TMY dataset, this is likely very long.

Source code in pvgisprototype/cli/meteo/tmy.py
def tmy(
    longitude: Annotated[float, typer_argument_longitude_in_degrees] = float(),
    latitude: Annotated[float, typer_argument_latitude_in_degrees] = float(),
    # time_series_2: Annotated[Path, typer_option_time_series] = None,
    timestamps: Annotated[DatetimeIndex, typer_argument_naive_timestamps] = str(
        now_datetime()
    ),
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = None,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    convert_longitude_360: Annotated[bool, typer_option_convert_longitude_360] = False,

    # Required variables
    # time_series: Annotated[Path, typer_argument_time_series],
    variable: Annotated[str | None, typer_option_data_variable] = None,
    meteorological_variable: Annotated[
        List[MeteorologicalVariable],
        typer.Option(
            help="Standard name of meteorological variable for Finkelstein-Schafer statistics"
        ),
    ] = [MeteorologicalVariable.all],
    global_horizontal_irradiance: Annotated[
        Path | None, typer_option_global_horizontal_irradiance
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None, typer_option_direct_horizontal_irradiance
    ] = None,
    temperature_series: Annotated[
        TemperatureSeries, typer_option_temperature_series_for_tmy
    ] = TemperatureSeries().average_air_temperature,
    relative_humidity_series: Annotated[
        RelativeHumiditySeries, typer_option_relative_humidity_series_for_tmy,
    ] = RelativeHumiditySeries().average_relative_humidity,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_option_wind_speed_series_for_tmy
    ] = WindSpeedSeries().average_wind_speed,
    # wind_speed_variable: Annotated[str | None, typer_option_data_variable] = None,

    # Solar positioning, required for the direct normal irradiance
    solar_position_model: SolarPositionModel = SOLAR_POSITION_ALGORITHM_DEFAULT,
    eccentricity_phase_offset: float = EccentricityPhaseOffset().value,
    eccentricity_amplitude: float = EccentricityAmplitude().value,

    # Series selection options
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = MethodForInexactMatches.nearest,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,

    # Output options
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    output_filename: Annotated[
        Path, typer_option_output_filename
    ] = "series_in",  # Path(),
    variable_name_as_suffix: Annotated[
        bool, typer_option_variable_name_as_suffix
    ] = True,

    # Optios for internal calculations
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,

    # More output options
    angle_output_units: Annotated[str, typer_option_angle_output_units] = RADIANS,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    weighting_scheme: TypicalMeteorologicalMonthWeightingScheme = TypicalMeteorologicalMonthWeightingScheme.ISO_15927_4.value,
    plot_statistic: Annotated[
        list[TMYStatisticModel],
        typer.Option(help="Select which Finkelstein-Schafer statistics to plot"),
    ] = None,  # [TMYStatisticModel.tmy.value],
    limit_x_axis_to_tmy_extent: Annotated[
        bool, "Limit plot of input time series to temporal extent of TMY"
    ] = True,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = QuickResponseCode.NoneValue,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
):
    """Generate the Typical Meteorological Year (TMY)

    A typical meteorological year (TMY) is a set of meteorological data with
    data values for every hour in a year for a given geographical location. The
    data are selected from hourly data in a longer time period (normally 10
    years or more). 

    An overview of the algorithm for calculating the TMY :

    1. **Input data** : Read _at least_ 10 years of hourly time series over a
    location

    2. **Daily Statistics** : Compute daily maximum, minimum, and mean for
    selected variables.

    3. **Cumulative Distribution Function** (CDF) : Compute the CDF of each
    variable for each month :

       3.1 one CDF for each variable, month, and year. For example, for the
       Global Horizontal Irradiance (GHI) : one for January 2011, one for
       January 2012 and so on, _and_ for each month the same for the ambient
       Temperature or other variables.

       3.2 one long-term CDF for each variable across all years for each month.

    4. **Finkelstein-Schafer Statistic** :

       4.1 Compute the absolute difference between the long-term CDF and the
       candidate month's CDF. 4.2 Compute the weighted sum (WS) of these
       differences for each month and year.

    5. **Typical Months** : Rank months by the lowest WS for each month (e.g.,
    rank all Januaries).

    6. **Month Selection** : 

       - ISO method: Select months based on wind speed similarity to the
         long-term average.

       - Sandia/NREL methods : Re-rank top months by their closeness to
         long-term averages, filtering based on extreme values.

    7. **TMY** : Combine the selected months into a continuous year, smoothing
    variable transitions at month boundaries.

    Notes
    -----

    Pay attention to the default _return_ modus of this function ! Without any
    `--verbose` asked, is to print all TMY variable values as one CSV string.
    In the case of a TMY dataset, this is likely very long.

    """
    direct_normal_irradiance_series = None
    direct_normal_irradiance = None

    # Map variables to their data series
    variable_series_map: Dict[MeteorologicalVariable, any] = {
        MeteorologicalVariable.MEAN_DRY_BULB_TEMPERATURE: temperature_series,
        MeteorologicalVariable.MEAN_RELATIVE_HUMIDITY: relative_humidity_series,
        MeteorologicalVariable.MEAN_WIND_SPEED: wind_speed_series,
        MeteorologicalVariable.GLOBAL_HORIZONTAL_IRRADIANCE: global_horizontal_irradiance,
        MeteorologicalVariable.DIRECT_NORMAL_IRRADIANCE: direct_normal_irradiance,
    }

    # meteorological_variable = MeteorologicalVariable.MEAN_DRY_BULB_TEMPERATURE
    meteorological_variables = select_meteorological_variables(
        MeteorologicalVariable, meteorological_variable
    )  # Using a callback fails!

    # Filter map to only variables requested

    filtered_variable_map = {
        var: data
        for var, data in variable_series_map.items()
        if var in meteorological_variables
    }

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        transient=True,
    ) as progress:
        progress.add_task(description="Calculating the TMY...", total=None)
        if isinstance(global_horizontal_irradiance, (str, Path)) and isinstance(
            direct_horizontal_irradiance, (str, Path)
        ):
            global_horizontal_irradiance = select_time_series(
                    time_series=global_horizontal_irradiance,
                    # longitude=longitude_for_selection,
                    # latitude=latitude_for_selection,
                    longitude=longitude,
                    latitude=latitude,
                    timestamps=timestamps,
                    # convert_longitude_360=convert_longitude_360,
                    neighbor_lookup=neighbor_lookup,
                    tolerance=tolerance,
                    mask_and_scale=mask_and_scale,
                    in_memory=in_memory,
                    verbose=0,  # no verbosity here by choice!
                    log=log,
                )
            direct_horizontal_irradiance = select_time_series(
                    time_series=direct_horizontal_irradiance,
                    # longitude=longitude_for_selection,
                    # latitude=latitude_for_selection,
                    longitude=longitude,
                    latitude=latitude,
                    timestamps=timestamps,
                    # convert_longitude_360=convert_longitude_360,
                    neighbor_lookup=neighbor_lookup,
                    tolerance=tolerance,
                    mask_and_scale=mask_and_scale,
                    in_memory=in_memory,
                    verbose=0,  # no verbosity here by choice!
                    log=log,
                )
            direct_normal_irradiance_series = calculate_direct_normal_from_horizontal_irradiance_series(
                direct_horizontal_irradiance=direct_horizontal_irradiance.values,
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                # timezone=timezone,
                solar_position_model=solar_position_model,
                eccentricity_phase_offset=eccentricity_phase_offset,
                eccentricity_amplitude=eccentricity_amplitude,
                # angle_output_units=angle_output_units,
                dtype=dtype,
                array_backend=array_backend,
                verbose=verbose,
                log=log,
                fingerprint=fingerprint,
            )
            direct_normal_irradiance_series = DataArray(
                direct_normal_irradiance_series.value,
                coords=[("time", timestamps)],
                name=direct_normal_irradiance_series.title,
            )
            # direct_normal_irradiance_series.attrs["units"] = "W/m^2"
            # direct_normal_irradiance_series.load()
        if isinstance(temperature_series, Path):
            temperature_series = select_time_series(
                    longitude=longitude,
                    latitude=latitude,
                    timestamps=timestamps,
                    time_series=temperature_series,
                    neighbor_lookup=neighbor_lookup,
                    tolerance=tolerance,
                    mask_and_scale=mask_and_scale,
                    in_memory=in_memory,
                    # dtype=dtype,
                    # array_backend=array_backend,
                    # multi_thread=multi_thread,
                    verbose=verbose,
                    log=log,
                )
        if isinstance(relative_humidity_series, Path):
            # relative_humidity_series = get_relative_humidity_series(
            relative_humidity_series = select_time_series(
                    longitude=longitude,
                    latitude=latitude,
                    timestamps=timestamps,
                    time_series=relative_humidity_series,
                    neighbor_lookup=neighbor_lookup,
                    tolerance=tolerance,
                    mask_and_scale=mask_and_scale,
                    in_memory=in_memory,
                    # dtype=dtype,
                    # array_backend=array_backend,
                    # multi_thread=multi_thread,
                    verbose=verbose,
                    log=log,
                )
        if isinstance(wind_speed_series, Path):
            # wind_speed_series = get_wind_speed_series(
            wind_speed_series = select_time_series(
                    longitude=longitude,
                    latitude=latitude,
                    timestamps=timestamps,
                    time_series=wind_speed_series,
                    neighbor_lookup=neighbor_lookup,
                    tolerance=tolerance,
                    mask_and_scale=mask_and_scale,
                    in_memory=in_memory,
                    # dtype=dtype,
                    # array_backend=array_backend,
                    # multi_thread=multi_thread,
                    verbose=verbose,
                    log=log,
                )

        tmy = calculate_tmy(
            meteorological_variables=meteorological_variables,
            temperature_series=temperature_series,
            relative_humidity_series=relative_humidity_series,
            wind_speed_series=wind_speed_series,
            # wind_speed_variable=wind_speed_variable,
            global_horizontal_irradiance=global_horizontal_irradiance,
            direct_normal_irradiance=direct_normal_irradiance_series,
            timestamps=timestamps,
            weighting_scheme=weighting_scheme,
            verbose=verbose,
            fingerprint=fingerprint,
        )
        if plot_statistic:

            tmy_statistics = select_tmy_models(
                enum_type=TMYStatisticModel,
                models=plot_statistic,
            )
            plot_requested_tmy_statistics(
                tmy_series=tmy,
                variable=variable,
                statistics=tmy_statistics,
                meteorological_variables=meteorological_variables,
                temperature_series=temperature_series,
                relative_humidity_series=relative_humidity_series,
                wind_speed_series=wind_speed_series,
                global_horizontal_irradiance=global_horizontal_irradiance,
                direct_normal_irradiance=direct_normal_irradiance_series,
                weighting_scheme=weighting_scheme.name,
                limit_x_axis_to_tmy_extent=limit_x_axis_to_tmy_extent,
            )

    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if quick_response_code.value != QuickResponseCode.NoneValue:
        print(f"[code]quick_response_code[/code] {NOT_IMPLEMENTED_CLI}")
        # from pvgisprototype.cli.print.qr import print_quick_response_code

        # print_quick_response_code(
        #     dictionary=tmy,
        #     longitude=longitude,
        #     latitude=latitude,
        #     elevation=False,
        #     surface_orientation=False,
        #     surface_tilt=False,
        #     timestamps=timestamps,
        #     rounding_places=rounding_places,
        # )
    if not quiet:
        if verbose > 0:
            print(f"[code]verbose[/code] {NOT_IMPLEMENTED_CLI}")
            for meteorological_variable, output in tmy.items():
                continue
        else:
            # When verbose=0, print TMY data as CSV
            for meteorological_variable in meteorological_variables:
                variable_output = tmy.get(meteorological_variable)
                if variable_output is None:
                    continue

                # Get the actual TMY DataArray, not the FS statistic
                tmy_dataarray = retrieve_nested_value(variable_output, TMYStatisticModel.tmy.value)
                if tmy_dataarray is not None:
                    # Flatten and print as CSV
                    flat_values = tmy_dataarray.values.flatten().astype(str)
                    csv_str = ",".join(flat_values)
                    print(csv_str)
    # if not quiet:
    #     if verbose > 0:
    #         print(f"[code]verbose[/code] {NOT_IMPLEMENTED_CLI}")
    #         for meteorological_variable, output in tmy.items():
    #             continue
    #         # from pvgisprototype.cli.print.irradiance import print_irradiance_table_2

    #         # print_irradiance_table_2(
    #         #     longitude=longitude,
    #         #     latitude=latitude,
    #         #     timestamps=timestamps,
    #         #     dictionary=tmy,
    #         #     # title=photovoltaic_power_output_series['Title'] + f" series {POWER_UNIT}",
    #         #     rounding_places=rounding_places,
    #         #     index=index,
    #         #     surface_orientation=True,
    #         #     surface_tilt=True,
    #         #     verbose=verbose,
    #         # )
    #     else:
    #         flat_list = []
    #         for meteorological_variable in meteorological_variables:
    #             statistics = tmy.get(meteorological_variable)
    #             for data_array in statistics.get(
    #                 FinkelsteinSchaferStatisticModel.ranked, NOT_AVAILABLE
    #             ):
    #                 flat_list.extend(data_array.values.flatten().astype(str))
    #             csv_str = ",".join(flat_list)
    #             print(csv_str)
    if statistics:
        print(f"[code]statistics[/code] {NOT_IMPLEMENTED_CLI}")
        # from pvgisprototype.api.series.statistics import print_series_statistics

        # print_series_statistics(
        #     data_array=tmy,
        #     timestamps=timestamps,
        #     groupby=groupby,
        #     title="Typical Meteorological Year",
        # )
    if uniplot:
        print(f"[code]uniplot[/code] {NOT_IMPLEMENTED_CLI}")
        # from pvgisprototype.api.plot import uniplot_data_array_series

        # uniplot_data_array_series(
        #     data_array=tmy[list(tmy.data_vars)[0]],
        #     # list_extra_data_arrays=individual_series,
        #     timestamps=timestamps,
        #     resample_large_series=resample_large_series,
        #     lines=True,
        #     supertitle="Typical Meteorological Year",
        #     title="Typical Meteorological Year",
        #     label="TMY",
        #     # extra_legend_labels=individual_labels,
        #     unit="?",
        #     terminal_width_fraction=terminal_width_fraction,
        # )
    if fingerprint:
        print(f"[code]fingerprint[/code] {NOT_IMPLEMENTED_CLI}")
        # from pvgisprototype.cli.print.fingerprint import print_finger_hash

        # print_finger_hash(dictionary=tmy[list(tmy.data_vars)[0]])
    if metadata:
        import click

        from pvgisprototype.cli.print.metadata import print_command_metadata

        print_command_metadata(context=click.get_current_context())
    # Call write_irradiance_csv() last : it modifies the input dictionary !
    if csv:
        print(f"[code]csv[/code] {NOT_IMPLEMENTED_CLI}")

tmy_weighting

tmy_weighting(
    meteorological_variable: Annotated[
        MeteorologicalVariable,
        Argument(
            help="Standard name of meteorological variable for Finkelstein-Schafer statistics"
        ),
    ] = None,
    weighting_scheme: Annotated[
        TypicalMeteorologicalMonthWeightingScheme,
        Option(help="Weighting scheme"),
    ] = ISO_15927_4.value,
)

Print the available weightings for a meteorological variable or full scheme.

Source code in pvgisprototype/cli/meteo/tmy.py
def tmy_weighting(
    meteorological_variable: Annotated[
        MeteorologicalVariable,
        typer.Argument(help="Standard name of meteorological variable for Finkelstein-Schafer statistics"),
    ] = None,
    weighting_scheme: Annotated[
    TypicalMeteorologicalMonthWeightingScheme,
    typer.Option(help="Weighting scheme"),
    ] = TypicalMeteorologicalMonthWeightingScheme.ISO_15927_4.value,
):
    """Print the available weightings for a meteorological variable or full scheme."""
    if meteorological_variable:
        meteorological_variables = select_meteorological_variables(
            MeteorologicalVariable, [meteorological_variable]
        )
    else:
        print(f"{weighting_scheme} :")
        meteorological_variables = [None]  # To handle printing the full scheme

    for meteorological_variable in meteorological_variables:
        print(
            get_typical_meteorological_month_weighting_scheme(
                weighting_scheme=weighting_scheme,
                meteorological_variable=meteorological_variable,
            )
        )

performance

Modules:

Name Description
broadband

CLI module to calculate the photovoltaic power output over a

broadband_multiple_surfaces

CLI module to calculate the photovoltaic power output over a

introduction
spectral
spectral_effect

Calculate the spectral factor

broadband

CLI module to calculate the photovoltaic power output over a location for a period in time.

Functions:

Name Description
photovoltaic_power_output_series

Estimate the photovoltaic power output for a location and a moment or period

photovoltaic_power_output_series

photovoltaic_power_output_series(
    ctx: Context,
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    global_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_global_horizontal_irradiance,
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_direct_horizontal_irradiance,
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries,
        typer_argument_spectral_factor_series,
    ] = SPECTRAL_FACTOR_DEFAULT,
    temperature_series: Annotated[
        TemperatureSeries, typer_option_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_option_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches | None,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor,
        typer_option_linke_turbidity_factor_series,
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[
        float | None, typer_option_albedo
    ] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel,
        typer_option_solar_position_model,
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel,
        typer_option_solar_incidence_model,
    ] = iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool,
        typer_option_zero_negative_solar_incidence_angle,
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[
        float, typer_option_solar_constant
    ] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    horizon_profile: Annotated[
        DataArray | None, typer_option_horizon_profile
    ] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model
    ] = pvgis,
    shading_states: Annotated[
        List[ShadingState], typer_option_shading_state
    ] = [all],
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel,
        typer_option_photovoltaic_module_model,
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,
    peak_power: Annotated[
        float, typer_option_photovoltaic_module_peak_power
    ] = PEAK_POWER_DEFAULT,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel,
        typer_option_pv_power_algorithm,
    ] = king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm,
        typer_option_module_temperature_algorithm,
    ] = faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = RADIANS,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    analysis: Annotated[
        bool, typer_option_analysis
    ] = ANALYSIS_FLAG_TRUE,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    nomenclature: Annotated[
        bool, typer_option_nomenclature
    ] = NOMENCLATURE_FLAG_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    version: Annotated[
        bool, typer_option_version
    ] = VERSION_FLAG_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = METADATA_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = NoneValue,
    profile: Annotated[
        bool, typer_option_profiling
    ] = cPROFILE_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
)

Estimate the photovoltaic power output for a location and a moment or period in time.

Estimate the photovoltaic power over a time series or an arbitrarily aggregated energy production of a PV system connected to the electricity grid (without battery storage) based on broadband solar irradiance, ambient temperature and wind speed.

Notes

The optional input parameters global_horizontal_irradiance and direct_horizontal_irradiance accept any Xarray-support data file format and mean the global and direct irradiance on the horizontal plane.

Inside the API, however, and for legibility, the same parameters in the functions that calculate the diffuse and direct components, are defined as global_horizontal_component and direct_horizontal_component. This is to avoid confusion at the function level. For example, the function calculate_diffuse_inclined_irradiance_series() can read the direct horizontal component (thus the name of it direct_horizontal_component as well as simulate it. The point is to make it clear that if the direct_horizontal_component parameter is True (which means the user has provided an external dataset), then read it using the select_time_series() function.

Source code in pvgisprototype/cli/performance/broadband.py
@log_function_call
def photovoltaic_power_output_series(
    ctx: typer.Context,
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(Timestamp.now()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    global_horizontal_irradiance: Annotated[
        Path | None, typer_option_global_horizontal_irradiance
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None, typer_option_direct_horizontal_irradiance
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries, typer_argument_spectral_factor_series
    ] = SPECTRAL_FACTOR_DEFAULT,  # Accept also list of float values ?
    temperature_series: Annotated[
        TemperatureSeries, typer_option_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_option_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches | None, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor, typer_option_linke_turbidity_factor_series
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[float | None, typer_option_albedo] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel, typer_option_solar_position_model
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel, typer_option_solar_incidence_model
    ] = SolarIncidenceModel.iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool, typer_option_zero_negative_solar_incidence_angle
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[float, typer_option_solar_constant] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    horizon_profile: Annotated[DataArray | None, typer_option_horizon_profile] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model] = ShadingModel.pvgis,  # for power generation : should be one !
    shading_states: Annotated[
            List[ShadingState], typer_option_shading_state] = [ShadingState.all],
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel, typer_option_photovoltaic_module_model
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,  # PhotovoltaicModuleModel.CSI_FREE_STANDING,
    peak_power: Annotated[float, typer_option_photovoltaic_module_peak_power] = PEAK_POWER_DEFAULT,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel, typer_option_pv_power_algorithm
    ] = PhotovoltaicModulePerformanceModel.king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm, typer_option_module_temperature_algorithm
    ] = ModuleTemperatureAlgorithm.faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    angle_output_units: Annotated[str, typer_option_angle_output_units] = RADIANS,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    analysis: Annotated[bool, typer_option_analysis] = ANALYSIS_FLAG_TRUE,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    nomenclature: Annotated[
        bool, typer_option_nomenclature
    ] = NOMENCLATURE_FLAG_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    version: Annotated[bool, typer_option_version] = VERSION_FLAG_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = METADATA_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = QuickResponseCode.NoneValue,
    profile: Annotated[bool, typer_option_profiling] = cPROFILE_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
):
    """Estimate the photovoltaic power output for a location and a moment or period
    in time.

    Estimate the photovoltaic power over a time series or an arbitrarily
    aggregated energy production of a PV system connected to the electricity
    grid (without battery storage) based on broadband solar irradiance, ambient
    temperature and wind speed.

    Notes
    -----
    The optional input parameters `global_horizontal_irradiance` and
    `direct_horizontal_irradiance` accept any Xarray-support data file format
    and mean the global and direct irradiance on the horizontal plane.

    Inside the API, however, and for legibility, the same parameters in the
    functions that calculate the diffuse and direct components, are defined as
    `global_horizontal_component` and `direct_horizontal_component`. This is to
    avoid confusion at the function level. For example, the function
    `calculate_diffuse_inclined_irradiance_series()` can read the direct
    horizontal component (thus the name of it `direct_horizontal_component` as
    well as simulate it.  The point is to make it clear that if the
    `direct_horizontal_component` parameter is True (which means the user has
    provided an external dataset), then read it using the
    `select_time_series()` function.

    """
    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        transient=True,
    ) as progress:
        progress.add_task(description="Calculating photovoltaic power output...", total=None)
        if isinstance(global_horizontal_irradiance, (str, Path)) and isinstance(
            direct_horizontal_irradiance, (str, Path)
        ):  # NOTE This is in the case everything is pathlike
            global_horizontal_irradiance_array, direct_horizontal_irradiance_array = (
                read_horizontal_irradiance_components_from_sarah(
                    shortwave=global_horizontal_irradiance,
                    direct=direct_horizontal_irradiance,
                    longitude=convert_float_to_degrees_if_requested(longitude, DEGREES),
                    latitude=convert_float_to_degrees_if_requested(latitude, DEGREES),
                    timestamps=timestamps,
                    neighbor_lookup=neighbor_lookup,
                    tolerance=tolerance,
                    mask_and_scale=mask_and_scale,
                    in_memory=in_memory,
                    multi_thread=multi_thread,
                    # multi_thread=False,
                    verbose=verbose,
                    log=log,
                )
            )
        else:  # Ensure the calculate() function below receices an array or None !
            global_horizontal_irradiance_array = None
            direct_horizontal_irradiance_array = None

        temperature_series, wind_speed_series, spectral_factor_series = get_time_series(
            temperature_series=temperature_series,
            wind_speed_series=wind_speed_series,
            spectral_factor_series=spectral_factor_series,
            timestamps=timestamps,
            longitude=Longitude(value=longitude, unit='radians'),
            latitude=Latitude(values=latitude, units='radians'),
            neighbor_lookup=neighbor_lookup,
            tolerance=tolerance,
            mask_and_scale=mask_and_scale,
            in_memory=in_memory,
            dtype=dtype,
            array_backend=array_backend,
            multi_thread=multi_thread,
            verbose=verbose,
            log=log,
        )
        photovoltaic_power_output_series = calculate_photovoltaic_power_output_series(
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            surface_orientation=surface_orientation,
            surface_tilt=surface_tilt,
            timestamps=timestamps,
            timezone=timezone,
            global_horizontal_irradiance=global_horizontal_irradiance_array,
            direct_horizontal_irradiance=direct_horizontal_irradiance_array,
            spectral_factor_series=spectral_factor_series,
            temperature_series=temperature_series,
            wind_speed_series=wind_speed_series,
            linke_turbidity_factor_series=linke_turbidity_factor_series,
            adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
            # unrefracted_solar_zenith=unrefracted_solar_zenith,
            albedo=albedo,
            apply_reflectivity_factor=apply_reflectivity_factor,
            solar_position_model=solar_position_model,
            solar_incidence_model=solar_incidence_model,
            zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
            horizon_profile=horizon_profile,
            shading_model=shading_model,
            shading_states=shading_states,
            solar_time_model=solar_time_model,
            solar_constant=solar_constant,
            eccentricity_phase_offset=eccentricity_phase_offset,
            eccentricity_amplitude=eccentricity_amplitude,
            # angle_output_units=angle_output_units,
            photovoltaic_module=photovoltaic_module,
            peak_power=peak_power,
            system_efficiency=system_efficiency,
            power_model=power_model,
            temperature_model=temperature_model,
            efficiency=efficiency,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            fingerprint=fingerprint,
            profile=profile,
            validate_output=validate_output,
        )  # Re-Design Me ! ------------------------------------------------

    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)

    if quick_response_code.value != QuickResponseCode.NoneValue:
        from pvgisprototype.cli.print.qr import print_quick_response_code

        print_quick_response_code(
            dictionary=photovoltaic_power_output_series.output,
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            surface_orientation=True,
            surface_tilt=True,
            timestamps=timestamps,
            rounding_places=rounding_places,
            output_type=quick_response_code,
        )
        return
    if not quiet:
        if verbose > 0:
            from pvgisprototype.cli.print.irradiance.data import print_irradiance_table_2

            print_irradiance_table_2(
                # title=photovoltaic_power_output_series['Title'] + f" series {POWER_UNIT}",
                irradiance_data=photovoltaic_power_output_series.output,
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                rounding_places=rounding_places,
                index=index,
                surface_orientation=True,
                surface_tilt=True,
                verbose=verbose,
            )
        else:
            # ------------------------- Better handling of rounding vs dtype ?
            print(
                ",".join(
                    # round_float_values(
                    #     photovoltaic_power_output_series.value.flatten(),
                    #     rounding_places,
                    # ).astype(str)
                    photovoltaic_power_output_series.value.flatten().astype(str)
                )
            )
    if statistics:
        from pvgisprototype.cli.print.series import print_series_statistics

        print_series_statistics(
            data_array=photovoltaic_power_output_series.value,
            timestamps=timestamps,
            groupby=groupby,
            title="Photovoltaic power output",
            rounding_places=rounding_places,
        )
    if analysis:
        from pvgisprototype.cli.print.performance.analysis import print_change_percentages_panel

        print_change_percentages_panel(
            photovoltaic_power=photovoltaic_power_output_series,#.output,
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            timestamps=timestamps,
            timezone=timezone,
            rounding_places=1,  # minimalism
            index=index,
            surface_orientation=True,
            surface_tilt=True,
            horizon_profile=horizon_profile,
            version=version,
            fingerprint=fingerprint,
            verbose=verbose,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_data_array_series

        uniplot_data_array_series(
            data_array=photovoltaic_power_output_series.value,
            list_extra_data_arrays=None,
            timestamps=timestamps,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle=photovoltaic_power_output_series.supertitle,
            title=photovoltaic_power_output_series.title,
            label=photovoltaic_power_output_series.label,
            extra_legend_labels=None,
            unit=POWER_UNIT,
            terminal_width_fraction=terminal_width_fraction,
        )
    if metadata:
        import click

        from pvgisprototype.cli.print.metadata import print_command_metadata

        print_command_metadata(context=click.get_current_context())
    if fingerprint and not analysis:
        from pvgisprototype.cli.print.fingerprint import print_finger_hash

        print_finger_hash(dictionary=photovoltaic_power_output_series.output)
    # Call write_irradiance_csv() last : it modifies the input dictionary !
    if csv:
        from pvgisprototype.cli.write import write_irradiance_csv

        write_irradiance_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dictionary=photovoltaic_power_output_series.output,
            filename=csv,
            index=index,
        )

broadband_multiple_surfaces

CLI module to calculate the photovoltaic power output over a location for a period in time.

Functions:

Name Description
photovoltaic_power_output_series_from_multiple_surfaces

Estimate the sum of photovoltaic output for multiple solar surface

photovoltaic_power_output_series_from_multiple_surfaces

photovoltaic_power_output_series_from_multiple_surfaces(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        list | None, typer_option_surface_orientation_multi
    ] = [float(SURFACE_ORIENTATION_DEFAULT)],
    surface_tilt: Annotated[
        list | None, typer_option_surface_tilt_multi
    ] = [float(SURFACE_TILT_DEFAULT)],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(DatetimeIndex([now()])),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    global_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_global_horizontal_irradiance,
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_direct_horizontal_irradiance,
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries,
        typer_argument_spectral_factor_series,
    ] = SPECTRAL_FACTOR_DEFAULT,
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor,
        typer_option_linke_turbidity_factor_series,
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[
        float | None, typer_option_albedo
    ] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel,
        typer_option_solar_position_model,
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel,
        typer_option_solar_incidence_model,
    ] = iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool,
        typer_option_zero_negative_solar_incidence_angle,
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    horizon_profile: Annotated[
        DataArray | None, typer_option_horizon_profile
    ] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model
    ] = pvgis,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[
        float, typer_option_solar_constant
    ] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = RADIANS,
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel,
        typer_option_photovoltaic_module_model,
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel,
        typer_option_pv_power_algorithm,
    ] = king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm,
        typer_option_module_temperature_algorithm,
    ] = faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    analysis: Annotated[
        bool, typer_option_analysis
    ] = ANALYSIS_FLAG_TRUE,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = NoneValue,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    profile: Annotated[
        bool, typer_option_profiling
    ] = cPROFILE_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
)

Estimate the sum of photovoltaic output for multiple solar surface setups for a location and a moment or period in time.

Estimate the sum of photovoltaic power over a time series or an arbitrarily aggregated energy production of multiple PV system setups, i.e. different tilts and orientations, connected to the electricity grid (without battery storage) based on broadband solar irradiance, ambient temperature and wind speed.

Notes

The optional input parameters global_horizontal_irradiance and direct_horizontal_irradiance accept any Xarray-support data file format and mean the global and direct irradiance on the horizontal plane.

Inside the API, however, and for legibility, the same parameters in the functions that calculate the diffuse and direct presentation, are defined as global_horizontal_component and direct_horizontal_component. This is to avoid confusion at the function level. For example, the function calculate_diffuse_inclined_irradiance_series() can read the direct horizontal component (thus the name of it direct_horizontal_component as well as simulate it. The point is to make it clear that if the direct_horizontal_component parameter is True (which means the user has provided an external dataset), then read it using the select_time_series() function.

Source code in pvgisprototype/cli/performance/broadband_multiple_surfaces.py
@log_function_call
def photovoltaic_power_output_series_from_multiple_surfaces(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        list | None, typer_option_surface_orientation_multi
    ] = [float(SURFACE_ORIENTATION_DEFAULT)],
    surface_tilt: Annotated[list | None, typer_option_surface_tilt_multi] = [
        float(SURFACE_TILT_DEFAULT)
    ],
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(DatetimeIndex([Timestamp.now()])),
    start_time: Annotated[datetime | None, typer_option_start_time] = None,
    periods: Annotated[int | None, typer_option_periods] = None,
    frequency: Annotated[str | None, typer_option_frequency] = None,
    end_time: Annotated[datetime | None, typer_option_end_time] = None,
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    global_horizontal_irradiance: Annotated[
        Path | None, typer_option_global_horizontal_irradiance
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None, typer_option_direct_horizontal_irradiance
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries, typer_argument_spectral_factor_series
    ] = SPECTRAL_FACTOR_DEFAULT,  # Accept also list of float values ?
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor, typer_option_linke_turbidity_factor_series
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[float | None, typer_option_albedo] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel, typer_option_solar_position_model
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel, typer_option_solar_incidence_model
    ] = SolarIncidenceModel.iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool, typer_option_zero_negative_solar_incidence_angle
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    horizon_profile: Annotated[DataArray | None, typer_option_horizon_profile] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model] = ShadingModel.pvgis,  # for power generation : should be one !
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[float, typer_option_solar_constant] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    angle_output_units: Annotated[str, typer_option_angle_output_units] = RADIANS,
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel, typer_option_photovoltaic_module_model
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,  # PhotovoltaicModuleModel.CSI_FREE_STANDING,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel, typer_option_pv_power_algorithm
    ] = PhotovoltaicModulePerformanceModel.king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm, typer_option_module_temperature_algorithm
    ] = ModuleTemperatureAlgorithm.faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    analysis: Annotated[bool, typer_option_analysis] = ANALYSIS_FLAG_TRUE,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = QuickResponseCode.NoneValue,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    profile: Annotated[bool, typer_option_profiling] = cPROFILE_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
):
    """Estimate the sum of photovoltaic output for multiple solar surface
    setups for a location and a moment or period in time.

    Estimate the sum of photovoltaic power over a time series or an arbitrarily
    aggregated energy production of multiple PV system setups, i.e. different
    tilts and orientations, connected to the electricity grid (without battery
    storage) based on broadband solar irradiance, ambient temperature and wind
    speed.

    Notes
    -----
    The optional input parameters `global_horizontal_irradiance` and
    `direct_horizontal_irradiance` accept any Xarray-support data file format
    and mean the global and direct irradiance on the horizontal plane.

    Inside the API, however, and for legibility, the same parameters in the
    functions that calculate the diffuse and direct presentation, are defined as
    `global_horizontal_component` and `direct_horizontal_component`. This is to
    avoid confusion at the function level. For example, the function
    `calculate_diffuse_inclined_irradiance_series()` can read the direct
    horizontal component (thus the name of it `direct_horizontal_component` as
    well as simulate it.  The point is to make it clear that if the
    `direct_horizontal_component` parameter is True (which means the user has
    provided an external dataset), then read it using the
    `select_time_series()` function.

    """
    if len(surface_tilt) != len(surface_orientation):
        from pvgisprototype.api.series.hardcodings import exclamation_mark

        logger.error(
            f"{exclamation_mark} Aborting as length of --surface_orientation and --surface_tilt is not the same!",
            alt=f"{exclamation_mark} [red]Aborting[/red] as [red]length[/red] [code]--surface-orientation[/code] and [code]--surface-tilt[/code] [red]is not the same[/red]!",
        )
        return

    photovoltaic_power_output_series = calculate_photovoltaic_power_output_series_from_multiple_surfaces(
        longitude=longitude,
        latitude=latitude,
        elevation=elevation,
        surface_orientation=surface_orientation,
        surface_tilt=surface_tilt,
        timestamps=timestamps,
        timezone=timezone,
        global_horizontal_irradiance=global_horizontal_irradiance,
        direct_horizontal_irradiance=direct_horizontal_irradiance,
        spectral_factor_series=spectral_factor_series,
        temperature_series=temperature_series,
        wind_speed_series=wind_speed_series,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        in_memory=in_memory,
        dtype=dtype,
        array_backend=array_backend,
        multi_thread=multi_thread,
        linke_turbidity_factor_series=linke_turbidity_factor_series,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        # unrefracted_solar_zenith=unrefracted_solar_zenith,
        albedo=albedo,
        apply_reflectivity_factor=apply_reflectivity_factor,
        solar_position_model=solar_position_model,
        solar_incidence_model=solar_incidence_model,
        zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
        horizon_height=horizon_profile,  # Review naming please ?
        horizon_profile=horizon_profile,
        shading_model=shading_model,
        shading_states=shading_states,
        solar_time_model=solar_time_model,
        solar_constant=solar_constant,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        angle_output_units=angle_output_units,
        photovoltaic_module=photovoltaic_module,
        system_efficiency=system_efficiency,
        power_model=power_model,
        temperature_model=temperature_model,
        efficiency=efficiency,
        verbose=verbose,
        log=log,
        fingerprint=fingerprint,
        profile=profile,
        validate_output=validate_output,

    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if quick_response_code.value != QuickResponseCode.NoneValue:
        from pvgisprototype.cli.print.qr import print_quick_response_code

        print_quick_response_code(
            dictionary=photovoltaic_power_output_series.presentation,
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            surface_orientation=True,
            surface_tilt=True,
            timestamps=timestamps,
            rounding_places=rounding_places,
        )
        return

    if not quiet:
        if verbose > 0:
            from pvgisprototype.cli.print.irradiance.data import print_irradiance_table_2

            print_irradiance_table_2(
                title=photovoltaic_power_output_series.title + f" series {POWER_UNIT}",
                irradiance_data=photovoltaic_power_output_series.presentation,
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                rounding_places=rounding_places,
                index=index,
                surface_orientation=True,
                surface_tilt=True,
                verbose=verbose,
            )
        else:
            flat_list = photovoltaic_power_output_series.series.flatten().astype(str)
            csv_str = ",".join(flat_list)
            print(csv_str)
    if statistics:
        from pvgisprototype.cli.print.series import print_series_statistics

        print_series_statistics(
            data_array=photovoltaic_power_output_series.value,
            timestamps=timestamps,
            groupby=groupby,
            title=photovoltaic_power_output_series.title,
        )
    if analysis:
        from pvgisprototype.cli.print.performance.anaysis import print_change_percentages_panel

        print_change_percentages_panel(
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            timestamps=timestamps,
            dictionary=photovoltaic_power_output_series.presentation,
            # title=photovoltaic_power_output_series['Title'] + f" series {POWER_UNIT}",
            rounding_places=1,  # minimalism
            index=index,
            surface_orientation=True,
            surface_tilt=True,
            fingerprint=fingerprint,
            verbose=verbose,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_data_array_series

        individual_series = [
            series.value
            for series in photovoltaic_power_output_series.individual_series
        ]
        surface_orientation = [
            convert_float_to_degrees_if_requested(orientation, angle_output_units)
            for orientation in surface_orientation
        ]
        surface_tilt = [
            convert_float_to_degrees_if_requested(tilt, angle_output_units)
            for tilt in surface_tilt
        ]
        surface_orientation = round_float_values(surface_orientation, rounding_places)
        surface_tilt = round_float_values(surface_tilt, rounding_places)
        individual_labels = [
            f"Orientation, Tilt : {orientation}°, {tilt}°"
            for orientation, tilt in zip(surface_orientation, surface_tilt)
        ]
        uniplot_data_array_series(
            data_array=photovoltaic_power_output_series.series,
            list_extra_data_arrays=individual_series,
            timestamps=timestamps,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle=photovoltaic_power_output_series.supertitle,
            title=photovoltaic_power_output_series.title,
            label=photovoltaic_power_output_series.label,
            extra_legend_labels=individual_labels,
            unit=POWER_UNIT,
            terminal_width_fraction=terminal_width_fraction,
        )
    if fingerprint and not analysis:
        from pvgisprototype.cli.print.fingerprint import print_finger_hash

        print_finger_hash(dictionary=photovoltaic_power_output_series.presentation)
    if metadata:
        import click

        from pvgisprototype.cli.print.metadata import print_command_metadata

        print_command_metadata(context=click.get_current_context())
    # Call write_irradiance_csv() last : it modifies the input dictionary !
    if csv:
        from pvgisprototype.cli.write import write_irradiance_csv

        write_irradiance_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dictionary=photovoltaic_power_output_series.presentation,
            filename=csv,
            index=index,
        )

introduction

Functions:

Name Description
photovoltaic_performance_introduction

A short introduction on photovoltaic performance

photovoltaic_performance_introduction

photovoltaic_performance_introduction()

A short introduction on photovoltaic performance

Source code in pvgisprototype/cli/performance/introduction.py
def photovoltaic_performance_introduction():
    """A short introduction on photovoltaic performance"""
    introduction = """
    [underline]The performance of a photovoltaic (PV) system[/underline] is ...
    """
    note = """
    PVGIS can estimate the performance of a series of photovoltaic technologies using either [magenta]broadband[/magenta] or [magenta]spectrally resolved[/magenta] irradiance data.
    """
    from rich.panel import Panel

    note_in_a_panel = Panel(
        "[italic]{}[/italic]".format(note),
        title="[bold cyan]Note[/bold cyan]",
        width=78,
    )
    from rich.console import Console

    console = Console()
    # introduction.wrap(console, 30)
    console.print(introduction)
    console.print(note_in_a_panel)
    console.print(A_PRIMER_ON_PHOTOVOLTAIC_PERFORMANCE)

spectral

Functions:

Name Description
spectral_photovoltaic_performance_analysis

This method accounts for the effects of the solar spectrum's varying

spectral_photovoltaic_performance_analysis

spectral_photovoltaic_performance_analysis(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None, typer_option_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_option_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        str | None, typer_option_timezone
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    spectrally_resolved_global_horizontal_irradiance_series: Annotated[
        Path | None,
        typer_option_global_horizontal_irradiance,
    ] = None,
    spectrally_resolved_direct_horizontal_irradiance_series: Annotated[
        Path | None,
        typer_option_direct_horizontal_irradiance,
    ] = None,
    number_of_junctions: int = 1,
    spectral_response_data: Path | None = None,
    standard_conditions_response: Path | None = None,
    minimum_spectral_mismatch=MINIMUM_SPECTRAL_MISMATCH,
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor,
        typer_option_linke_turbidity_factor_series,
    ] = None,
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[
        float | None, typer_option_albedo
    ] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = True,
    solar_position_model: Annotated[
        SolarPositionModel,
        typer_option_solar_position_model,
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel,
        typer_option_solar_incidence_model,
    ] = iqbal,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[
        float, typer_option_solar_constant
    ] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    time_output_units: Annotated[
        str, typer_option_time_output_units
    ] = MINUTES,
    angle_units: Annotated[
        str, typer_option_angle_units
    ] = RADIANS,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = RADIANS,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel,
        typer_option_pv_power_algorithm,
    ] = king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm,
        typer_option_module_temperature_algorithm,
    ] = faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
)

This method accounts for the effects of the solar spectrum's varying wavelengths on PV output, offering a more detailed analysis for systems sensitive to specific spectral ranges.

Source code in pvgisprototype/cli/performance/spectral.py
def spectral_photovoltaic_performance_analysis(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None, typer_option_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_option_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[datetime | None, typer_option_start_time] = None,
    periods: Annotated[int | None, typer_option_periods] = None,
    frequency: Annotated[str | None, typer_option_frequency] = None,
    end_time: Annotated[datetime | None, typer_option_end_time] = None,
    timezone: Annotated[str | None, typer_option_timezone] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    spectrally_resolved_global_horizontal_irradiance_series: Annotated[
        Path | None, typer_option_global_horizontal_irradiance
    ] = None,
    spectrally_resolved_direct_horizontal_irradiance_series: Annotated[
        Path | None, typer_option_direct_horizontal_irradiance
    ] = None,
    number_of_junctions: int = 1,
    spectral_response_data: Path | None = None,
    standard_conditions_response: Path | None = None,  #: float = 1,  # STCresponse : read from external data
    # extraterrestrial_normal_irradiance_series,  # spectral_ext,
    minimum_spectral_mismatch=MINIMUM_SPECTRAL_MISMATCH,
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    # dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    # array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    # multi_thread: Annotated[bool, typer_option_multi_thread] = MULTI_THREAD_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor, typer_option_linke_turbidity_factor_series
    ] = None,  # Changed this to np.ndarray
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[float | None, typer_option_albedo] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = True,
    solar_position_model: Annotated[
        SolarPositionModel, typer_option_solar_position_model
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel, typer_option_solar_incidence_model
    ] = SolarIncidenceModel.iqbal,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[float, typer_option_solar_constant] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    time_output_units: Annotated[str, typer_option_time_output_units] = MINUTES,
    angle_units: Annotated[str, typer_option_angle_units] = RADIANS,
    angle_output_units: Annotated[str, typer_option_angle_output_units] = RADIANS,
    # horizon_heights: Annotated[List[float], typer.Argument(help="Array of horizon elevations.")] = None,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel, typer_option_pv_power_algorithm
    ] = PhotovoltaicModulePerformanceModel.king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm, typer_option_module_temperature_algorithm
    ] = ModuleTemperatureAlgorithm.faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
):
    """
    This method accounts for the effects of the solar spectrum's varying
    wavelengths on PV output, offering a more detailed analysis for systems
    sensitive to specific spectral ranges.
    """
    (
        spectrally_resolved_photovoltaic_power,
        results,
        title,
    ) = calculate_spectral_photovoltaic_power_output(
        longitude=longitude,
        latitude=latitude,
        elevation=elevation,
        timestamps=timestamps,
        timezone=timezone,
        spectrally_resolved_global_horizontal_irradiance_series=spectrally_resolved_global_horizontal_irradiance_series,
        spectrally_resolved_direct_horizontal_irradiance_series=spectrally_resolved_direct_horizontal_irradiance_series,
        spectral_response_data=spectral_response_data,
        number_of_junctions=number_of_junctions,
        standard_conditions_response=standard_conditions_response,
        minimum_spectral_mismatch=minimum_spectral_mismatch,
        temperature_series=temperature_series,
        wind_speed_series=wind_speed_series,
        mask_and_scale=mask_and_scale,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        in_memory=in_memory,
        surface_orientation=surface_orientation,
        surface_tilt=surface_tilt,
        linke_turbidity_factor_series=linke_turbidity_factor_series,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        unrefracted_solar_zenith=unrefracted_solar_zenith,
        albedo=albedo,
        apply_reflectivity_factor=apply_reflectivity_factor,
        solar_position_model=solar_position_model,
        solar_incidence_model=solar_incidence_model,
        solar_time_model=solar_time_model,
        solar_constant=solar_constant,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        time_output_units=time_output_units,
        angle_units=angle_units,
        angle_output_units=angle_output_units,
        system_efficiency=system_efficiency,
        power_model=power_model,
        temperature_model=temperature_model,
        efficiency=efficiency,
        verbose=verbose,
    )
    # longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    # latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if not quiet:
        if verbose > 0:
            pass
        #     print_irradiance_table_2(
        #         longitude=longitude,
        #         latitude=latitude,
        #         timestamps=timestamps,
        #         dictionary=results,
        #         title=title + f' irradiance series {IRRADIANCE_UNIT}',
        #         rounding_places=rounding_places,
        #         index=index,
        #         verbose=verbose,
        #     )
        else:
            flat_list = spectrally_resolved_photovoltaic_power.flatten().astype(str)
            csv_str = ",".join(flat_list)
            print(csv_str)

spectral_effect

Calculate the spectral factor

Functions:

Name Description
spectral_factor

spectral_factor

spectral_factor(
    irradiance: Annotated[
        Path, typer_argument_spectrally_resolved_irradiance
    ],
    longitude: Annotated[
        float, typer_argument_longitude_in_degrees
    ],
    latitude: Annotated[
        float, typer_argument_latitude_in_degrees
    ],
    elevation: Annotated[float, typer_argument_elevation],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        str | None, typer_option_timezone
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    responsivity: Annotated[
        SpectralResponsivity,
        typer_option_spectral_responsivity_pandas,
    ] = SPECTRAL_RESPONSIVITY_DATA,
    integrate_responsivity: Annotated[
        bool, typer_option_integrate_spectral_responsivity
    ] = False,
    responsivity_column: Annotated[
        str, typer_option_responsivity_column_name
    ] = SPECTRAL_RESPONSIVITY_CSV_COLUMN_NAME_DEFAULT,
    wavelength_column: Annotated[
        str, typer_option_wavelength_column_name
    ] = WAVELENGTHS_CSV_COLUMN_NAME_DEFAULT,
    photovoltaic_module_type: Annotated[
        List[PhotovoltaicModuleSpectralResponsivityModel],
        typer_option_photovoltaic_module_type,
    ] = [cSi],
    spectrally_resolved_irradiance: Annotated[
        str, typer_option_data_variable
    ] = "",
    average_irradiance_density: Annotated[
        str, typer_option_data_variable
    ] = "",
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    limit_spectral_range: Annotated[
        bool,
        Option(
            help="Limit the spectral range of the irradiance input data. Default for `spectral_factor_model = Pelland`"
        ),
    ] = False,
    min_wavelength: Annotated[
        float,
        typer_option_minimum_spectral_irradiance_wavelength,
    ] = MIN_WAVELENGTH,
    max_wavelength: Annotated[
        float,
        typer_option_maximum_spectral_irradiance_wavelength,
    ] = MAX_WAVELENGTH,
    reference_spectrum: Annotated[
        None | DataFrame, typer_option_reference_spectrum
    ] = None,
    integrate_reference_spectrum: Annotated[
        bool, typer_option_integrate_reference_spectrum
    ] = False,
    spectral_factor_model: Annotated[
        List[SpectralMismatchModel],
        typer_option_spectral_factor_model,
    ] = [pvlib],
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    analysis: Annotated[
        bool, typer_option_analysis
    ] = ANALYSIS_FLAG_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    show_footer: Annotated[
        bool, Option(help="Show output table footer")
    ] = True,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = METADATA_FLAG_DEFAULT,
)
Source code in pvgisprototype/cli/performance/spectral_effect.py
@log_function_call
def spectral_factor(
    irradiance: Annotated[
        Path,
        typer_argument_spectrally_resolved_irradiance,
    ],
    longitude: Annotated[float, typer_argument_longitude_in_degrees],
    latitude: Annotated[float, typer_argument_latitude_in_degrees],
    elevation: Annotated[float, typer_argument_elevation],
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[str | None, typer_option_timezone] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    responsivity: Annotated[
        SpectralResponsivity,
        typer_option_spectral_responsivity_pandas,
    ] = SPECTRAL_RESPONSIVITY_DATA,  # Accept also list of float values ?
    integrate_responsivity: Annotated[
        bool,
        typer_option_integrate_spectral_responsivity,
    ] = False,
    responsivity_column: Annotated[
        str,
        typer_option_responsivity_column_name,
    ] = SPECTRAL_RESPONSIVITY_CSV_COLUMN_NAME_DEFAULT,
    wavelength_column: Annotated[
        str,
        typer_option_wavelength_column_name,
    ] = WAVELENGTHS_CSV_COLUMN_NAME_DEFAULT,
    photovoltaic_module_type: Annotated[
        List[PhotovoltaicModuleSpectralResponsivityModel],
        typer_option_photovoltaic_module_type,
    ] = [PhotovoltaicModuleSpectralResponsivityModel.cSi],
    spectrally_resolved_irradiance: Annotated[str, typer_option_data_variable] = "",
    average_irradiance_density: Annotated[str, typer_option_data_variable] = "",
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    limit_spectral_range: Annotated[
        bool,
        typer.Option(
            help="Limit the spectral range of the irradiance input data. Default for `spectral_factor_model = Pelland`"
        ),
    ] = False,
    min_wavelength: Annotated[
        float, typer_option_minimum_spectral_irradiance_wavelength
    ] = MIN_WAVELENGTH,
    max_wavelength: Annotated[
        float, typer_option_maximum_spectral_irradiance_wavelength
    ] = MAX_WAVELENGTH,
    reference_spectrum: Annotated[
        None | DataFrame,
        typer_option_reference_spectrum,
    ] = None,  # AM15G_IEC60904_3_ED4,
    integrate_reference_spectrum: Annotated[
        bool,
        typer_option_integrate_reference_spectrum,
    ] = False,
    spectral_factor_model: Annotated[
        List[SpectralMismatchModel], typer_option_spectral_factor_model
    ] = [SpectralMismatchModel.pvlib],
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    analysis: Annotated[bool, typer_option_analysis] = ANALYSIS_FLAG_DEFAULT,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    show_footer: Annotated[bool, typer.Option(help="Show output table footer")] = True,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = METADATA_FLAG_DEFAULT,
):
    """ """
    # Ugly Hacks ! -----------------------------------------------------------
    from pvgisprototype.api.position.models import select_models

    photovoltaic_module_type = select_models(
        PhotovoltaicModuleSpectralResponsivityModel, photovoltaic_module_type
    )  # Using a callback fails!

    # ------------------------------------------------------------------------
    def is_netcdf(file_path: Path) -> bool:
        return file_path.suffix in {".nc", ".netcdf"}

    if is_netcdf(irradiance):
        # irradiance = (
        #     select_time_series(
        #         time_series=irradiance,
        #         longitude=longitude,
        #         latitude=latitude,
        #         timestamps=timestamps,
        #         neighbor_lookup=neighbor_lookup,
        #         tolerance=tolerance,
        #         mask_and_scale=mask_and_scale,
        #         in_memory=in_memory,
        #         verbose=verbose,
        #         log=log,
        #     )
        #     .to_numpy()
        #     .astype(dtype=dtype)
        # )
        spectrally_resolved_irradiance = select_time_series(
            time_series=irradiance,
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            variable=spectrally_resolved_irradiance,
            neighbor_lookup=neighbor_lookup,
            tolerance=tolerance,
            mask_and_scale=mask_and_scale,
            in_memory=in_memory,
            verbose=verbose,
            log=log,
        )
        if (
            SpectralMismatchModel.pvlib in spectral_factor_model
        ):
            logger.debug(
                f"Average irradiance density :\n{average_irradiance_density}",
                alt=f"[bold]Average irradiance density[/bold] :\n{average_irradiance_density}",
            )
            if average_irradiance_density:
                average_irradiance_density = select_time_series(
                    time_series=irradiance,
                    longitude=longitude,
                    latitude=latitude,
                    timestamps=timestamps,
                    variable=average_irradiance_density,
                    neighbor_lookup=neighbor_lookup,
                    tolerance=tolerance,
                    mask_and_scale=mask_and_scale,
                    in_memory=in_memory,
                    verbose=verbose,
                    log=log,
                )

            # # Ugly Hack! --------------------------------------------------- #
            # if not isinstance(average_irradiance_density, DataArray) and isinstance(spectrally_resolved_irradiance, DataArray):
            #     spectrally_resolved_irradiance = spectrally_resolved_irradiance.rename(
            #         {wavelength_column: "wavelength"}
            #     )
            # # Ugly Hack! --------------------------------------------------- #

    if limit_spectral_range:
        import numpy

        if numpy.any(
            numpy.logical_or(
                irradiance[wavelength_column] < min_wavelength,
                irradiance[wavelength_column] > max_wavelength,
            )
        ):
            logger.debug(
                f"{check_mark} The input irradiance wavelengths are within the reference range [{min_wavelength}, {max_wavelength}]."
            )
        else:
            logger.warning(
                f"{x_mark} The input irradiance wavelengths exceed the reference range [{min_wavelength}, {max_wavelength}]. Filtering..."
            )
            irradiance = irradiance.sel(
                center_wavelength=numpy.logical_and(
                    irradiance[wavelength_column] > min_wavelength,
                    irradiance[wavelength_column] < max_wavelength,
                )
            )
    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        transient=True,
    ) as progress:
        progress.add_task(description="Calculating spectral factor...", total=None)
        spectral_factor_series = calculate_spectral_factor(
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            timestamps=timestamps,
            timezone=timezone,
            irradiance=spectrally_resolved_irradiance,
            average_irradiance_density=average_irradiance_density,
            responsivity=responsivity,
            photovoltaic_module_type=photovoltaic_module_type,
            reference_spectrum=reference_spectrum,
            integrate_reference_spectrum=integrate_reference_spectrum,
            spectral_factor_models=spectral_factor_model,
            verbose=verbose,
            log=log,
            fingerprint=fingerprint,
        )
    # longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    # latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if not quiet:
        if verbose > 0:
            from pvgisprototype.cli.print.spectral_factor import print_spectral_factor

            print_spectral_factor(
                timestamps=timestamps,
                spectral_factor_container=spectral_factor_series.components,
                spectral_factor_model=spectral_factor_model,
                photovoltaic_module_type=photovoltaic_module_type,
                # include_statistics=statistics,
                title="Spectral Factor",
                verbose=verbose,
                index=index,
                show_footer=show_footer,
            )
        else:
            spectral_factor_dictionary = {}
            for model in spectral_factor_model:
                for module_type in photovoltaic_module_type:
                    mismatch_data = (
                        spectral_factor_series.components.get(model)
                        .get(module_type)
                        .get(SPECTRAL_FACTOR_COLUMN_NAME)
                    )
                    if isinstance(mismatch_data, memoryview):
                        import numpy

                        mismatch_data = (numpy.array(mismatch_data).values.flatten(),)
                    # # ------------------------- Better handling of rounding vs dtype ?
                    #     from pvgisprototype.api.utilities.conversions import round_float_values
                    #     mismatch_data = round_float_values(
                    #                 mismatch_data,
                    #             rounding_places,
                    #         ).astype(str)
                    # # ------------------------- Better handling of rounding vs dtype ?
                    spectral_factor_dictionary[module_type.name] = mismatch_data

                header = ", ".join(spectral_factor_dictionary.keys())
                print(header)

                # mismatch_values = ", ".join(mismatch_data.astype(str))
                # print(f'{module_type.name}, {mismatch_values}')

                # maximum length of mismatch data to properly format rows
                max_length = max([len(v) for v in spectral_factor_dictionary.values()])

                # Print each row of mismatch values, transposing the values
                for i in range(max_length):
                    row = []
                    for module_type in spectral_factor_dictionary:
                        if i < len(spectral_factor_dictionary[module_type]):
                            row.append(
                                f"{spectral_factor_dictionary[module_type][i]:.6f}"
                            )
                        else:
                            row.append("")  # Handle cases where lengths are uneven
                    print(", ".join(row))

    if csv:
        from pvgisprototype.cli.write import write_spectral_factor_csv

        write_spectral_factor_csv(
            longitude=None,
            latitude=None,
            timestamps=timestamps,
            spectral_factor_dictionary=spectral_factor_series.components,
            filename=csv,
            index=index,
        )
    if statistics:
        from pvgisprototype.api.series.statistics import (
            print_spectral_factor_statistics,
        )

        print_spectral_factor_statistics(
            spectral_factor=spectral_factor_series.components,
            spectral_factor_model=spectral_factor_model,
            photovoltaic_module_type=photovoltaic_module_type,
            timestamps=timestamps,
            # groupby=groupby,
            title="Spectral Factor Statistics",
            rounding_places=rounding_places,
            verbose=verbose,
            show_footer=show_footer,
        )
    # if analysis:
    #     from pvgisprototype.cli.print import print_change_percentages_panel

    #     print_change_percentages_panel(
    #         longitude=longitude,
    #         latitude=latitude,
    #         elevation=elevation,
    #         timestamps=timestamps,
    #         dictionary=photovoltaic_power_output_series.components,
    #         # title=photovoltaic_power_output_series['Title'] + f" series {POWER_UNIT}",
    #         rounding_places=1,  # minimalism
    #         index=index,
    #         surface_orientation=True,
    #         surface_tilt=True,
    #         fingerprint=fingerprint,
    #         verbose=verbose,
    #     )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_spectral_factor_series

        uniplot_spectral_factor_series(
            spectral_factor_dictionary=spectral_factor_series.value,
            spectral_factor_model=spectral_factor_model,
            photovoltaic_module_type=photovoltaic_module_type,
            timestamps=timestamps,
            resample_large_series=resample_large_series,
            # lines=True,
            # supertitle="Spectral Mismatch Factor",
            supertitle=spectral_factor_series.supertitle,
            # title="Spectral Factor",
            title=spectral_factor_series.title,
            # label=photovoltaic_module_types,
            # label=spectral_factor_series.label,
            # extra_legend_labels=None,
            # unit='',
            terminal_width_fraction=terminal_width_fraction,
        )
    if metadata:
        import click

        from pvgisprototype.cli.print import print_command_metadata

        print_command_metadata(context=click.get_current_context())
    if fingerprint and not analysis:
        from pvgisprototype.cli.print.fingerprint import print_finger_hash

        print_finger_hash(dictionary=spectral_factor_series.presentation)

plot

Modules:

Name Description
horizon
uniplot

This module contains code adapted from spiel by Josh Karpel

horizon

Functions:

Name Description
plot_horizon_profile_x

Plot a horizon height profile dynamically in polar coordinates along with

plot_horizon_profile_x

plot_horizon_profile_x(
    solar_position_series: dict,
    horizon_profile: NDArray,
    labels: List[str],
    colors=["cyan", "blue", "yellow"],
)

Plot a horizon height profile dynamically in polar coordinates along with positions of the sun in the sky (solar altitude).

Parameters:

Name Type Description Default
solar_position_series dict

Dictionary containing solar position data.

required
horizon_profile NDArray

Array of horizon height values corresponding to azimuth angles.

required
labels List[str]

List of labels for the plot legend.

required
colors List[str]

List of colors for plotting different series (default is ["cyan", "blue", "yellow"]).

['cyan', 'blue', 'yellow']
Notes

This function creates a polar plot showing the horizon height profile around a given geographic location and positions of the sun in the sky (solar altitude). The horizon height profile is represented as a radial plot, where the radial distance starting from the outer perimeter (horizontal plane circle) corresponds to the horizon height, and the clock-wise angle starting from the top center of the polar plot (0 degrees = North) represents the azimuthal direction.

Examples:

To use this function via the command line:

>>> pvgis-prototype position overview 7.9672 45.9684 --start-time "2010-06-15 05:00:00" --end-time "2010-06-15 20:00:00"  --horizon-profile horizon_12_076.zarr --horizon-plot

This command will generate a polar plot, as shown below, showing the horizon profile and solar positions for the requested location and period.

┌─────────────────────────────────────────────┐ │ ⣀⠤⠤⠒⠒⢀⡠⣀⣀⡀⠉⠉⠉⠉⠒⠒⠤⠤⣀ │ │ ⣀⠤⠒⠉ ⣀⡠⠔⠒⠁ ⠠⣀⡀ ⠉⠒⠤⣀ │ │ ⢀⠤⠊ ⢀⠖⠉ ⠈⠑⠢⢄⡀ ⠑⠤⡀ │ │ ⢀⠔⠁ ⢀⡠⠒⠁ ⠈⠓⠤⡀ ⠈⠢⡀ │ │ ⢀⠁ ⢀⡐⠉ ⠈⠢⣀ ⠈⢆ │ │ ⡜ ⢀⡜ ⠑⣄ ⢣ │ │ ⡜ ⡀⢸ ⢢ ⢀ ⢣ │ │⢰⠁ ⡜ ⢇ ⠈⡆│ │⡇ ⠐ ⠈⡆⠄ ⢸│ │⡇ ⢱ ⢀ ⢀ ⡇ ⢸│ │⡇ ⢣ ⢀ ⡀ ⢠⠃ ⢸│ │⡇ ⢣ ⠠ ⠄ ⡎ ⢸│ │⢸⡀ ⢸ ⠁ ⠐ ⠄ ⠐ ⢰⠁ ⢀⡇│ │ ⢣ ⢣ ⡔⠁ ⡜ │ │ ⢣ ⠡⡄ ⡸ ⡜ │ │ ⠱⡀ ⠈⢆ ⠠⠊ ⢀⠎ │ │ ⠈⠢⡀ ⠑⠢⢄⡀ ⢀⡠⠔⠊⠁ ⢀⠔⠁ │ │ ⠈⠢⢄ ⠈⠑⠢⢄⣀⣀⣀⣀⣀⠤⠒⠉⠉⠁ ⡠⠔⠁ │ │ ⠉⠢⢄⡀ ⢀⡠⠔⠉ │ │ ⠈⠉⠒⠢⠤⢄⣀⣀⣀⣀⣀⣀⣀⣀⣀⡠⠤⠔⠒⠉⠁ │ └─────────────────────────────────────────────┘

Acknowledgment

Some assistance by : - Olav Stetter - Chrysa Stathaki

Source code in pvgisprototype/cli/plot/horizon.py
def plot_horizon_profile_x(
    solar_position_series: dict,
    horizon_profile: NDArray,
    labels: List[str],
    colors=["cyan", "blue", "yellow"],
):

    """
    Plot a horizon height profile dynamically in polar coordinates along with
    positions of the sun in the sky (solar altitude).

    Parameters
    ----------
    solar_position_series : dict
        Dictionary containing solar position data.
    horizon_profile : NDArray
        Array of horizon height values corresponding to azimuth angles.
    labels : List[str]
        List of labels for the plot legend.
    colors : List[str], optional
        List of colors for plotting different series (default is ["cyan", "blue", "yellow"]).

    Notes
    -----
    This function creates a polar plot showing the horizon height profile
    around a given geographic location and positions of the sun in the sky
    (solar altitude). The horizon height profile is represented as a radial
    plot, where the radial distance starting from the outer perimeter
    (horizontal plane circle) corresponds to the horizon height, and the
    clock-wise angle starting from the top center of the polar plot (0 degrees
    = North) represents the azimuthal direction.

    Examples
    --------
    To use this function via the command line:

    >>> pvgis-prototype position overview 7.9672 45.9684 --start-time "2010-06-15 05:00:00" --end-time "2010-06-15 20:00:00"  --horizon-profile horizon_12_076.zarr --horizon-plot

    This command will generate a polar plot, as shown below, showing the
    horizon profile and solar positions for the requested location and period.

    ┌─────────────────────────────────────────────┐
    │             ⣀⠤⠤⠒⠒⢀⡠⣀⣀⡀⠉⠉⠉⠉⠒⠒⠤⠤⣀             │
    │         ⣀⠤⠒⠉ ⣀⡠⠔⠒⠁   ⠠⣀⡀       ⠉⠒⠤⣀         │
    │      ⢀⠤⠊  ⢀⠖⠉          ⠈⠑⠢⢄⡀       ⠑⠤⡀      │
    │    ⢀⠔⠁ ⢀⡠⠒⠁                ⠈⠓⠤⡀      ⠈⠢⡀    │
    │   ⢀⠁ ⢀⡐⠉                      ⠈⠢⣀      ⠈⢆   │
    │  ⡜  ⢀⡜                           ⠑⣄      ⢣  │
    │ ⡜  ⡀⢸                              ⢢   ⢀  ⢣ │
    │⢰⠁   ⡜                               ⢇     ⠈⡆│
    │⡇    ⠐                               ⠈⡆⠄    ⢸│
    │⡇    ⢱ ⢀                            ⢀ ⡇     ⢸│
    │⡇     ⢣   ⢀                       ⡀  ⢠⠃     ⢸│
    │⡇      ⢣     ⠠                 ⠄     ⡎      ⢸│
    │⢸⡀     ⢸         ⠁  ⠐   ⠄  ⠐        ⢰⠁     ⢀⡇│
    │ ⢣      ⢣                          ⡔⠁      ⡜ │
    │  ⢣      ⠡⡄                       ⡸       ⡜  │
    │   ⠱⡀     ⠈⢆                    ⠠⠊      ⢀⠎   │
    │    ⠈⠢⡀     ⠑⠢⢄⡀            ⢀⡠⠔⠊⠁     ⢀⠔⠁    │
    │      ⠈⠢⢄      ⠈⠑⠢⢄⣀⣀⣀⣀⣀⠤⠒⠉⠉⠁       ⡠⠔⠁      │
    │         ⠉⠢⢄⡀                   ⢀⡠⠔⠉         │
    │            ⠈⠉⠒⠢⠤⢄⣀⣀⣀⣀⣀⣀⣀⣀⣀⡠⠤⠔⠒⠉⠁            │
    └─────────────────────────────────────────────┘

    Acknowledgment
    --------------
    Some assistance by :
    - Olav Stetter
    - Chrysa Stathaki

    """
    from pvgisprototype.cli.print.getters import get_value_or_default
    from pvgisprototype.api.position.models import (
        SolarPositionParameter,
    )

    # Generate equidistant azimuthal directions

    azimuthal_directions_radians = np.linspace(0, 2 * np.pi, horizon_profile.size)
    plot(
        xs=np.degrees(azimuthal_directions_radians),
        ys=horizon_profile,
        lines=True,
        width=45,
        height=3,
        x_gridlines=[],
        y_gridlines=[],
        color=[colors[1]],
        legend_labels=[labels[1]],
        character_set="braille",
    )

    # Calculate polar coordinates (x, y) for the horitontal plane and horizon height profile

    horizon_profile_radians = np.radians(horizon_profile)  # input in degrees
    x_horizontal_plane = np.sin(azimuthal_directions_radians) * np.pi / 2
    y_horizontal_plane = np.cos(azimuthal_directions_radians) * np.pi / 2
    x_horizon = x_horizontal_plane - np.sin(azimuthal_directions_radians) * horizon_profile_radians
    y_horizon = y_horizontal_plane - np.cos(azimuthal_directions_radians) * horizon_profile_radians

    # Loop over possibly multiple solar positioning algorithms ?
    for model_name, model_result in solar_position_series.items():

        # Get altitude
        solar_altitude_in_radians = get_value_or_default(
                model_result,
                SolarPositionParameterColumnName.altitude
                )

        # Mask values outside the azimuth circle.  Attention : overwrite original variables !
        solar_altitude_in_radians = where(
                solar_altitude_in_radians >= 0,
                solar_altitude_in_radians,
                nan
        )

        # Get azimuth

        solar_azimuth_radians = get_value_or_default(
                model_result,
                SolarPositionParameterColumnName.azimuth
                )
        # Convert solar azimuth in degrees series to radians

        # Calculate polar coordinates (x, y) for solar azimuth and altitude

        x_solar_azimuth = np.sin(solar_azimuth_radians) * np.pi / 2
        y_solar_azimuth = np.cos(solar_azimuth_radians) * np.pi / 2

        x_polar_solar_altitude = x_solar_azimuth - np.sin(solar_azimuth_radians) * solar_altitude_in_radians
        y_polar_solar_altitude = y_solar_azimuth - np.cos(solar_azimuth_radians) * solar_altitude_in_radians

        x_series = [
            x_horizontal_plane,
            x_horizon,
            x_polar_solar_altitude,
        ]
        y_series = [
            y_horizontal_plane,
            y_horizon,
            y_polar_solar_altitude,
        ]

        # Plot the horizon profile polar coordinates in Cartesian space
        plot(
            xs=x_series,
            ys=y_series,
            lines=[True, True, False],
            width=45,
            height=20,
            x_gridlines=[],
            y_gridlines=[],
            color=colors,
            legend_labels=labels,
            character_set="braille",
        )

uniplot

position

Modules:

Name Description
altitude

CLI module to calculate the solar altitude angle parameters over a

azimuth
declination

CLI module to calculate the solar declination angle for a location and moment in time.

event_time
hour_angle
incidence

CLI module to calculate the solar incidence angle for a solar surface at a

introduction
overview

CLI module to calculate and overview the solar position parameters over a

position

Important sun and solar surface position parameters in calculating the amount of solar radiation that reaches a particular location on the Earth's surface

shading

CLI module to calculate and overview the solar position parameters over a

zenith

CLI module to calculate the solar zenith angle for a location and a single moment in time.

altitude

CLI module to calculate the solar altitude angle parameters over a location and a moment in time.

Functions:

Name Description
altitude

Calculate the solar altitude angle above the horizon.

altitude

altitude(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    model: Annotated[
        list[SolarPositionModel],
        typer_option_solar_position_model,
    ] = [noaa],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = milne,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool,
        "Visually cluster time series results per model",
    ] = False,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    panels: Annotated[
        bool, typer_option_panels_output
    ] = False,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
) -> SolarAltitude

Calculate the solar altitude angle above the horizon.

The solar altitude angle (SAA) is the complement of the solar zenith angle, measuring from the horizon directly below the sun to the sun itself. An altitude of 0 degrees means the sun is on the horizon, and an altitude of 90 degrees means the sun is directly overhead.

Source code in pvgisprototype/cli/position/altitude.py
@log_function_call
def altitude(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    model: Annotated[list[SolarPositionModel], typer_option_solar_position_model] = [
        SolarPositionModel.noaa
    ],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SolarTimeModel.milne,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool, "Visually cluster time series results per model"
    ] = False,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    panels: Annotated[bool, typer_option_panels_output] = False,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,

) -> SolarAltitude:
    """Calculate the solar altitude angle above the horizon.

    The solar altitude angle (SAA) is the complement of the solar zenith angle,
    measuring from the horizon directly below the sun to the sun itself. An
    altitude of 0 degrees means the sun is on the horizon, and an altitude of
    90 degrees means the sun is directly overhead.

    Parameters
    ----------
    """
    utc_timestamps = convert_timestamps_to_utc(
        user_requested_timezone=timezone,
        user_requested_timestamps=timestamps,
    )
    # Why does the callback function `_parse_model` not work?
    solar_position_models = select_models(
        SolarPositionModel, model
    )  # Using a callback fails!
    solar_altitude_series = calculate_solar_altitude_series(
        longitude=longitude,
        latitude=latitude,
        timestamps=utc_timestamps,
        timezone=utc_timestamps.tz,
        solar_position_models=solar_position_models,
        # solar_time_model=solar_time_model,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        angle_output_units=angle_output_units,
        array_backend=array_backend,
        dtype=dtype,
        verbose=verbose,
        validate_output=validate_output,
        fingerprint=fingerprint,
    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if not quiet:
        from pvgisprototype.cli.print.position.data import print_solar_position_series_table

        print_solar_position_series_table(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_altitude_series,
            position_parameters=[SolarPositionParameter.altitude],
            title="Solar Altitude Series",
            index=index,
            surface_orientation=None,
            surface_tilt=None,
            incidence=None,
            user_requested_timestamps=timestamps,
            user_requested_timezone=timezone,
            rounding_places=rounding_places,
            group_models=group_models,
            panels=panels,
        )
    if csv:
        from pvgisprototype.cli.write import write_solar_position_series_csv

        write_solar_position_series_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_altitude_series,
            position_parameters=solar_position_parameters,
            # user_requested_timestamps=timestamps,
            # user_requested_timezone=timezone,
            index=index,
            rounding_places=rounding_places,
            # group_models=group_models,
            filename=csv,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_solar_position_series

        uniplot_solar_position_series(
            solar_position_series=solar_altitude_series,
            position_parameters=[SolarPositionParameter.altitude],
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            longitude=longitude,
            latitude=latitude,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Solar Altitude Series",
            title="Solar Altitude",
            label="Altitude",
            legend_labels=None,
            terminal_width_fraction=terminal_width_fraction,
            verbose=verbose,
        )

azimuth

Functions:

Name Description
azimuth

Calculate the solar azimuth angle.

azimuth

azimuth(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    model: Annotated[
        List[SolarPositionModel],
        typer_option_solar_position_model,
    ] = [noaa],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = milne,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool,
        "Visually cluster time series results per model",
    ] = False,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    panels: Annotated[
        bool, typer_option_panels_output
    ] = False,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
)

Calculate the solar azimuth angle.

The solar azimuth angle (Az) specifies the east-west orientation of the sun. It is usually measured from the south, going positive to the west. The exact definitions can vary, with some sources defining the azimuth with respect to the north, so care must be taken to use the appropriate convention.

Parameters:

Name Type Description Default
Returns
required
solar_azimuth
required
Source code in pvgisprototype/cli/position/azimuth.py
@log_function_call
def azimuth(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    model: Annotated[List[SolarPositionModel], typer_option_solar_position_model] = [
        SolarPositionModel.noaa
    ],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SolarTimeModel.milne,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool, "Visually cluster time series results per model"
    ] = False,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    panels: Annotated[bool, typer_option_panels_output] = False,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
):
    """Calculate the solar azimuth angle.

    The solar azimuth angle (Az) specifies the east-west orientation of the
    sun. It is usually measured from the south, going positive to the west. The
    exact definitions can vary, with some sources defining the azimuth with
    respect to the north, so care must be taken to use the appropriate
    convention.

    Parameters
    ----------

    Returns
    -------
    solar_azimuth: float
    """
    utc_timestamps = convert_timestamps_to_utc(
        user_requested_timezone=timezone,
        user_requested_timestamps=timestamps,
    )
    # Why does the callback function `_parse_model` not work?
    solar_position_models = select_models(
        SolarPositionModel, model
    )  # Using a callback fails!
    solar_azimuth_series = calculate_solar_azimuth_series(
        longitude=longitude,
        latitude=latitude,
        timestamps=utc_timestamps,
        timezone=utc_timestamps.tz,
        solar_position_models=solar_position_models,
        solar_time_model=solar_time_model,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        angle_output_units=angle_output_units,
        verbose=verbose,
        validate_output=validate_output,
    )

    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if not quiet:
        from pvgisprototype.cli.print.position.data import print_solar_position_series_table

        print_solar_position_series_table(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_azimuth_series,
            position_parameters=[SolarPositionParameter.azimuth],
            title="Solar Azimuth Series",
            index=index,
            surface_orientation=None,
            surface_tilt=None,
            incidence=None,
            user_requested_timestamps=timestamps,
            user_requested_timezone=timezone,
            rounding_places=rounding_places,
            group_models=group_models,
            panels=panels,
        )
    if csv:
        from pvgisprototype.cli.write import write_solar_position_series_csv

        write_solar_position_series_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_azimuth_series,
            position_parameters=[SolarPositionParameter.azimuth],
            # user_requested_timestamps=timestamps,
            # user_requested_timezone=timezone,
            index=index,
            rounding_places=rounding_places,
            # group_models=group_models,
            filename=csv,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_solar_position_series

        uniplot_solar_position_series(
            solar_position_series=solar_azimuth_series,
            position_parameters=[SolarPositionParameter.azimuth],
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Solar Azimuth Series",
            title="Solar Azimuth",
            label="Azimuth",
            legend_labels=None,
            terminal_width_fraction=terminal_width_fraction,
            verbose=verbose,
        )

declination

CLI module to calculate the solar declination angle for a location and moment in time.

Functions:

Name Description
declination

Calculate the solar declination angle

declination

declination(
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = TIMEZONE_DEFAULT,
    local_time: Annotated[
        bool, typer_option_local_time
    ] = False,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    solar_declination_model: Annotated[
        List[SolarDeclinationModel],
        typer_option_solar_declination_model,
    ] = [pvis],
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool,
        "Visually cluster time series results per model",
    ] = False,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    panels: Annotated[
        bool, typer_option_panels_output
    ] = False,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
) -> None

Calculate the solar declination angle

The solar declination (delta) is the angle between the line from the Earth to the Sun and the plane of the Earth's equator. It varies between ±23.45 degrees over the course of a year as the Earth orbits the Sun.

Parameters:

Name Type Description Default
Returns
required
solar_declination
required
Source code in pvgisprototype/cli/position/declination.py
@log_function_call
def declination(
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = TIMEZONE_DEFAULT,
    local_time: Annotated[bool, typer_option_local_time] = False,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    solar_declination_model: Annotated[
        List[SolarDeclinationModel], typer_option_solar_declination_model
    ] = [SolarDeclinationModel.pvis],
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    angle_output_units: Annotated[str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool, "Visually cluster time series results per model"
    ] = False,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    panels: Annotated[bool, typer_option_panels_output] = False,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
) -> None:
    """Calculate the solar declination angle

    The solar declination (delta) is the angle between the line from the Earth
    to the Sun and the plane of the Earth's equator. It varies between ±23.45
    degrees over the course of a year as the Earth orbits the Sun.

    Parameters
    ----------

    Returns
    -------
    solar_declination: float
    """
    utc_timestamps = convert_timestamps_to_utc(
        user_requested_timezone=timezone,
        user_requested_timestamps=timestamps,
    )
    # Why does the callback function `_parse_model` not work?
    solar_declination_models = select_models(
        SolarDeclinationModel, solar_declination_model
    )  # Using a callback fails!
    solar_declination_series = calculate_solar_declination_series(
        timestamps=utc_timestamps,
        timezone=utc_timestamps.tz,
        solar_declination_models=solar_declination_models,
        eccentricity_amplitude=eccentricity_amplitude,
        eccentricity_phase_offset=eccentricity_phase_offset,
        angle_output_units=angle_output_units,
        array_backend=array_backend,
        dtype=dtype,
        verbose=verbose,
        validate_output=validate_output,
    )
    if not quiet:
        from pvgisprototype.cli.print.position.data import print_solar_position_series_table

        print_solar_position_series_table(
            longitude=None,
            latitude=None,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_declination_series,
            title="Solar Declination Angle",
            index=index,
            surface_orientation=None,
            surface_tilt=None,
            incidence=None,
            user_requested_timestamps=timestamps,
            user_requested_timezone=timezone,
            rounding_places=rounding_places,
            position_parameters=[SolarPositionParameter.declination],
            group_models=group_models,
            panels=panels,
        )
    if csv:
        from pvgisprototype.cli.write import write_solar_position_series_csv

        write_solar_position_series_csv(
            longitude=None,
            latitude=None,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_declination_series,
            position_parameters=solar_position_parameters,
            # user_requested_timestamps=timestamps,
            # user_requested_timezone=timezone,
            index=index,
            rounding_places=rounding_places,
            # group_models=group_models,
            filename=csv,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_solar_position_series

        uniplot_solar_position_series(
            solar_position_series=solar_declination_series,
            position_parameters=[SolarPositionParameter.declination],
            timestamps=utc_timestamps,
            # surface_orientation=True,
            # surface_tilt=True,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Solar Declination Series",
            title="Solar Declination",
            label="Declination",
            legend_labels=None,
            terminal_width_fraction=terminal_width_fraction,
            verbose=verbose,
        )

event_time

Functions:

Name Description
event_time

event_time

event_time(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = TIMEZONE_DEFAULT,
    event: Annotated[
        List[SolarEvent], typer_option_solar_event
    ] = [None],
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    solar_position_model: Annotated[
        List[SolarPositionModel],
        typer_option_solar_position_model,
    ] = [noaa],
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = milne,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool,
        "Visually cluster time series results per model",
    ] = False,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    panels: Annotated[
        bool, typer_option_panels_output
    ] = False,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
)
Source code in pvgisprototype/cli/position/event_time.py
@log_function_call
def event_time(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = TIMEZONE_DEFAULT,
    event: Annotated[List[SolarEvent], typer_option_solar_event] = [None],
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    solar_position_model: Annotated[
        List[SolarPositionModel], typer_option_solar_position_model
    ] = [SolarPositionModel.noaa],
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SolarTimeModel.milne,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool, "Visually cluster time series results per model"
    ] = False,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    panels: Annotated[bool, typer_option_panels_output] = False,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
):
    """
    """
    utc_timestamps = convert_timestamps_to_utc(
        user_requested_timezone=timezone,
        user_requested_timestamps=timestamps,
    )
    solar_event_time_series = calculate_event_time_series(
        longitude=longitude,
        latitude=latitude,
        timestamps=utc_timestamps,
        timezone=utc_timestamps.tz,
        event=event,
        dtype=dtype,
        array_backend=array_backend,
        # validate_output=validate_output,
        verbose=verbose,
        log=log,
    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    if not quiet:
        from pvgisprototype.cli.print.position.data import print_solar_position_series_table

        print_solar_position_series_table(
            longitude=longitude,
            latitude=None,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_event_time_series,
            position_parameters=[SolarPositionParameter.event_type, SolarPositionParameter.event_time],
            title="Solar Event Time",
            index=index,
            surface_orientation=True,
            surface_tilt=True,
            incidence=True,
            user_requested_timestamps=timestamps,
            user_requested_timezone=timezone,
            rounding_places=rounding_places,
            group_models=group_models,
            panels=panels,
        )
    if csv:
        from pvgisprototype.cli.write import write_solar_position_series_csv

        write_solar_position_series_csv(
            longitude=longitude,
            latitude=None,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_event_time_series,
            position_parameters=solar_position_parameters,
            # user_requested_timestamps=timestamps,
            # user_requested_timezone=timezone,
            index=index,
            rounding_places=rounding_places,
            # group_models=group_models,
            filename=csv,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_solar_position_series

        uniplot_solar_position_series(
            solar_position_series=solar_hour_angle_series,
            position_parameters=[SolarPositionParameter.hour_angle],
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            surface_orientation=True,
            surface_tilt=True,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Solar Hour Angle Series",
            title="Solar Hour Angle",
            label="Hour Angle",
            legend_labels=None,
            terminal_width_fraction=terminal_width_fraction,
            verbose=verbose,
        )

hour_angle

Functions:

Name Description
hour_angle

Calculate the hour angle 'ω = (ST / 3600 - 12) * 15 * π / 180'

hour_angle

hour_angle(
    longitude: Annotated[float, typer_argument_longitude],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        str | None, typer_option_timezone
    ] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    solar_position_model: Annotated[
        List[SolarPositionModel],
        typer_option_solar_position_model,
    ] = [noaa],
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = milne,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool,
        "Visually cluster time series results per model",
    ] = False,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    panels: Annotated[
        bool, typer_option_panels_output
    ] = False,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
)

Calculate the hour angle 'ω = (ST / 3600 - 12) * 15 * π / 180'

The hour angle (ω) is the angle at any instant through which the earth has to turn to bring the meridian of the observer directly in line with the sun's rays measured in radian. In other words, it is a measure of time, expressed in angular measurement, usually degrees, from solar noon. It increases by 15° per hour, negative before solar noon and positive after solar noon.

Source code in pvgisprototype/cli/position/hour_angle.py
@log_function_call
def hour_angle(
    longitude: Annotated[float, typer_argument_longitude],
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[str | None, typer_option_timezone] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    solar_position_model: Annotated[
        List[SolarPositionModel], typer_option_solar_position_model
    ] = [SolarPositionModel.noaa],
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SolarTimeModel.milne,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool, "Visually cluster time series results per model"
    ] = False,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    panels: Annotated[bool, typer_option_panels_output] = False,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
):
    """Calculate the hour angle 'ω = (ST / 3600 - 12) * 15 * π / 180'

    The hour angle (ω) is the angle at any instant through which the earth has
    to turn to bring the meridian of the observer directly in line with the
    sun's rays measured in radian. In other words, it is a measure of time,
    expressed in angular measurement, usually degrees, from solar noon. It
    increases by 15° per hour, negative before solar noon and positive after
    solar noon.
    """
    utc_timestamps = convert_timestamps_to_utc(
        user_requested_timezone=timezone,
        user_requested_timestamps=timestamps,
    )
    # Why does the callback function `_parse_model` not work?
    solar_position_models = select_models(
        SolarPositionModel, solar_position_model
    )  # Using a callback fails!
    solar_hour_angle_series = calculate_solar_hour_angle_series(
        longitude=longitude,
        timestamps=utc_timestamps,
        timezone=utc_timestamps.tz,
        solar_position_models=solar_position_models,
        solar_time_model=solar_time_model,
        angle_output_units=angle_output_units,
        dtype=dtype,
        array_backend=array_backend,
        verbose=verbose,
        log=log,
        validate_output=validate_output,
    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    if not quiet:
        from pvgisprototype.cli.print.position.data import print_solar_position_series_table

        print_solar_position_series_table(
            longitude=longitude,
            latitude=None,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_hour_angle_series,
            position_parameters=[SolarPositionParameter.hour_angle],
            title="Solar Position Overview",
            index=index,
            surface_orientation=True,
            surface_tilt=True,
            incidence=True,
            user_requested_timestamps=timestamps,
            user_requested_timezone=timezone,
            rounding_places=rounding_places,
            group_models=group_models,
            panels=panels,
        )
    if csv:
        from pvgisprototype.cli.write import write_solar_position_series_csv

        write_solar_position_series_csv(
            longitude=longitude,
            latitude=None,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_hour_angle_series,
            position_parameters=solar_position_parameters,
            # user_requested_timestamps=timestamps,
            # user_requested_timezone=timezone,
            index=index,
            rounding_places=rounding_places,
            # group_models=group_models,
            filename=csv,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_solar_position_series

        uniplot_solar_position_series(
            solar_position_series=solar_hour_angle_series,
            position_parameters=[SolarPositionParameter.hour_angle],
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            surface_orientation=True,
            surface_tilt=True,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Solar Hour Angle Series",
            title="Solar Hour Angle",
            label="Hour Angle",
            legend_labels=None,
            terminal_width_fraction=terminal_width_fraction,
            verbose=verbose,
        )

incidence

CLI module to calculate the solar incidence angle for a solar surface at a location, orientation, tilt and moment in time.

Functions:

Name Description
incidence

Calculate the angle of solar incidence

incidence

incidence(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    random_surface_orientation: Annotated[
        bool, typer_option_random_surface_orientation
    ] = False,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    random_surface_tilt: Annotated[
        bool, typer_option_random_surface_tilt
    ] = False,
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    solar_incidence_model: Annotated[
        List[SolarIncidenceModel],
        typer_option_solar_incidence_model,
    ] = [iqbal],
    horizon_profile: Annotated[
        DataArray | None, typer_option_horizon_profile
    ] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model
    ] = pvgis,
    complementary_incidence_angle: Annotated[
        bool,
        typer_option_sun_to_surface_plane_incidence_angle,
    ] = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = milne,
    zero_negative_solar_incidence_angle: Annotated[
        bool,
        typer_option_zero_negative_solar_incidence_angle,
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool,
        "Visually cluster time series results per model",
    ] = False,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    panels: Annotated[
        bool, typer_option_panels_output
    ] = False,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
)

Calculate the angle of solar incidence

The angle of incidence (also known as theta) is the angle between the direct beam of sunlight and the line perpendicular (normal) to the surface. If the sun is directly overhead and the surface is flat (horizontal), the angle of incidence is 0°.

Source code in pvgisprototype/cli/position/incidence.py
@log_function_call
def incidence(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    random_surface_orientation: Annotated[
        bool, typer_option_random_surface_orientation
    ] = False,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    random_surface_tilt: Annotated[
        bool, typer_option_random_surface_tilt
    ] = False,
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    solar_incidence_model: Annotated[
        List[SolarIncidenceModel], typer_option_solar_incidence_model
    ] = [SolarIncidenceModel.iqbal],
    horizon_profile: Annotated[DataArray | None, typer_option_horizon_profile] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model] = ShadingModel.pvgis,  # for 'overview' : should be one !
    complementary_incidence_angle: Annotated[
        bool, typer_option_sun_to_surface_plane_incidence_angle
    ] = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SolarTimeModel.milne,
    zero_negative_solar_incidence_angle: Annotated[
        bool, typer_option_zero_negative_solar_incidence_angle
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool, "Visually cluster time series results per model"
    ] = False,
    # statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    panels: Annotated[bool, typer_option_panels_output] = False,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
):
    """Calculate the angle of solar incidence

    The angle of incidence (also known as theta) is the angle between the
    direct beam of sunlight and the line perpendicular (normal) to the surface.
    If the sun is directly overhead and the surface is flat (horizontal), the
    angle of incidence is 0°.
    """
    utc_timestamps = convert_timestamps_to_utc(
        user_requested_timezone=timezone,
        user_requested_timestamps=timestamps,
    )
    # --------------------------------------------------------------- Idea ---
    # if not given, optimise tilt and orientation... ?
    # ------------------------------------------------------------------------

    if random_surface_orientation:
        import random

        surface_tilt = random.vonmisesvariate(pi, kappa=0)  # radians

    if random_surface_tilt:
        import random

        surface_tilt = random.uniform(0, pi / 2)  # radians

    solar_incidence_models = select_models(
        SolarIncidenceModel, solar_incidence_model
    )  # Using a callback fails!
    solar_incidence_series = calculate_solar_incidence_series(
        longitude=longitude,
        latitude=latitude,
        timestamps=utc_timestamps,
        timezone=utc_timestamps.tz,
        surface_orientation=SurfaceOrientation(
            value=surface_orientation, unit=RADIANS
        ),  # Typer does not easily support custom types !
        surface_tilt=SurfaceTilt(
            value=surface_tilt, unit=RADIANS
        ),  # Typer does not easily support custom types !
        solar_incidence_models=solar_incidence_models,
        horizon_profile=horizon_profile,
        shading_model=shading_model,
        complementary_incidence_angle=complementary_incidence_angle,
        zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
        # solar_time_model=solar_time_model,
        eccentricity_amplitude=eccentricity_amplitude,
        eccentricity_phase_offset=eccentricity_phase_offset,
        angle_output_units=angle_output_units,
        dtype=dtype,
        array_backend=array_backend,
        verbose=verbose,
        validate_output=validate_output,
    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if not quiet:
        from pvgisprototype.cli.print.position.data import print_solar_position_series_table

        print_solar_position_series_table(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            table=solar_incidence_series,
            position_parameters=[SolarPositionParameter.incidence],
            title="Solar Incidence",
            index=index,
            surface_orientation=None,
            surface_tilt=None,
            incidence=True,
            user_requested_timestamps=timestamps,
            user_requested_timezone=timezone,
            rounding_places=rounding_places,
            group_models=group_models,
            panels=panels,
        )
        # from pvgisprototype.cli.print import print_solar_incidence_series_in_columns
        # print_solar_incidence_series_in_columns(
        #         longitude=longitude,
        #         latitude=latitude,
        #         timestamps=timestamps,
        #         timezone=timezone,
        #         table=solar_incidence_series,
        #         )

    if csv:
        from pvgisprototype.cli.write import write_solar_position_series_csv

        write_solar_position_series_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_incidence_series,
            position_parameters=solar_position_parameters,
            # user_requested_timestamps=timestamps,
            # user_requested_timezone=timezone,
            index=index,
            rounding_places=rounding_places,
            # group_models=group_models,
            filename=csv,
        )

    if uniplot:
        from pvgisprototype.api.plot import uniplot_solar_position_series

        uniplot_solar_position_series(
            solar_position_series=solar_incidence_series,
            position_parameters=[SolarPositionParameter.incidence],
            timestamps=timestamps,  # User requested or UTC ?
            surface_orientation=None,
            surface_tilt=None,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Solar Position Series",
            title="Solar Position",
            label="Incidence",
            legend_labels=None,
            terminal_width_fraction=terminal_width_fraction,
            verbose=verbose,
        )

introduction

Functions:

Name Description
introduction

A short introduction on solar position

introduction

introduction()

A short introduction on solar position

Source code in pvgisprototype/cli/position/introduction.py
def introduction():
    """A short introduction on solar position"""
    introduction = """
    [underline]Solar position[/underline] consists of a series of angular
    measurements between the position of the sun in the sky and a location on
    the surface of the earth for a moment or a period in time.
    """

    note = """Internally, [bold]timestamps[/bold] are converted to [magenta]UTC[/magenta] and [bold]angles[/bold] are measured in [magenta]radians[/magenta] !
    """
    from rich.panel import Panel

    note_in_a_panel = Panel(
        "[italic]{}[/italic]".format(note),
        title="[bold cyan]Note[/bold cyan]",
        width=78,
    )
    from rich.console import Console

    console = Console()
    # introduction.wrap(console, 30)
    console.print(introduction)
    console.print(note_in_a_panel)
    console.print(A_PRIMER_ON_SOLAR_GEOMETRY)

overview

CLI module to calculate and overview the solar position parameters over a location for a period in time.

Functions:

Name Description
overview

overview

overview(
    ctx: Context,
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    random_surface_orientation: Annotated[
        bool, typer_option_random_surface_orientation
    ] = False,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    random_surface_tilt: Annotated[
        bool, typer_option_random_surface_tilt
    ] = False,
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = TIMEZONE_DEFAULT,
    event: Annotated[
        List[SolarEvent] | None, typer_option_solar_event
    ] = [sunrise, sunset],
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    solar_position_model: Annotated[
        List[SolarPositionModel],
        typer_option_solar_position_model,
    ] = [noaa],
    position_parameter: Annotated[
        List[SolarPositionParameter],
        typer_option_solar_position_parameter,
    ] = [all],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    sun_horizon_position: Annotated[
        List[SunHorizonPositionModel],
        typer_option_sun_horizon_position,
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = milne,
    complementary_incidence_angle: Annotated[
        bool,
        typer_option_sun_to_surface_plane_incidence_angle,
    ] = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: Annotated[
        bool,
        typer_option_zero_negative_solar_incidence_angle,
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    horizon_profile: Annotated[
        DataArray | None, typer_option_horizon_profile
    ] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model
    ] = pvgis,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool,
        "Visually cluster time series results per model",
    ] = False,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    horizon_plot: Annotated[
        bool, typer_option_horizon_profile_plot
    ] = False,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    version: Annotated[
        bool, typer_option_version
    ] = VERSION_FLAG_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    panels: Annotated[
        bool, typer_option_panels_output
    ] = False,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
) -> None
Source code in pvgisprototype/cli/position/overview.py
@log_function_call
def overview(
    ctx: typer.Context,
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    random_surface_orientation: Annotated[
        bool, typer_option_random_surface_orientation
    ] = False,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    random_surface_tilt: Annotated[
        bool, typer_option_random_surface_tilt
    ] = False,
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = TIMEZONE_DEFAULT,
    event: Annotated[List[SolarEvent] | None, typer_option_solar_event] = [SolarEvent.sunrise, SolarEvent.sunset],
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    solar_position_model: Annotated[
        List[SolarPositionModel], typer_option_solar_position_model
    ] = [SolarPositionModel.noaa],
    position_parameter: Annotated[
        List[SolarPositionParameter], typer_option_solar_position_parameter
    ] = [SolarPositionParameter.all],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    sun_horizon_position: Annotated[
            List[SunHorizonPositionModel], typer_option_sun_horizon_position
    ] = SUN_HORIZON_POSITION_DEFAULT,
    # refracted_solar_zenith: Annotated[
    #     float | None, typer_option_refracted_solar_zenith
    # ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SolarTimeModel.milne,
    # solar_incidence_model: Annotated[SolarIncidenceModel, typer_option_solar_incidence_model] = SolarIncidenceModel.iqbal,
    complementary_incidence_angle: Annotated[
        bool, typer_option_sun_to_surface_plane_incidence_angle
    ] = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: Annotated[
        bool, typer_option_zero_negative_solar_incidence_angle
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    horizon_profile: Annotated[DataArray | None, typer_option_horizon_profile] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model] = ShadingModel.pvgis,  # for 'overview' : should be one !
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool, "Visually cluster time series results per model"
    ] = False,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    horizon_plot: Annotated[
        bool, typer_option_horizon_profile_plot
    ] = False,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    version: Annotated[bool, typer_option_version] = VERSION_FLAG_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    panels: Annotated[bool, typer_option_panels_output] = False,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
) -> None:
    """ """
    utc_timestamps = convert_timestamps_to_utc(
        user_requested_timezone=timezone,
        user_requested_timestamps=timestamps,
    )
    # Why does the callback function `_parse_model` not work?
    solar_position_models = select_models(
        SolarPositionModel, solar_position_model
    )  # Using a callback fails!
    # Why does the callback function `_parse_model` not work?

    solar_position_series = calculate_solar_position_overview_series(
        longitude=longitude,
        latitude=latitude,
        timestamps=utc_timestamps,
        timezone=utc_timestamps.tz,
        event=event,
        surface_orientation=surface_orientation,
        surface_tilt=surface_tilt,
        solar_position_models=solar_position_models,
        sun_horizon_position=sun_horizon_position,
        horizon_profile=horizon_profile,
        shading_model=shading_model,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        # unrefracted_solar_zenith=unrefracted_solar_zenith,
        solar_time_model=solar_time_model,
        # solar_incidence_model=solar_incidence_model,
        complementary_incidence_angle=complementary_incidence_angle,
        zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        # time_output_units=time_output_units,
        angle_output_units=angle_output_units,
        dtype=dtype,
        array_backend=array_backend,
        verbose=verbose,
        validate_output=validate_output,
        fingerprint=fingerprint,
    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    position_parameter = ctx.params.get(
        "position_parameter"
    )  # Bug in Typer that is not passing correctly whatever is in the context ?
    solar_position_parameters = select_models(
        SolarPositionParameter, position_parameter
    )  # Using a callback fails!
    if not quiet:
        from pvgisprototype.cli.print.position.data import print_solar_position_series_table

        print_solar_position_series_table(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_position_series,
            position_parameters=solar_position_parameters,
            title="Solar Position Overview",
            index=index,
            version=version,
            fingerprint=fingerprint,
            surface_orientation=True,
            surface_tilt=True,
            incidence=True,
            user_requested_timestamps=timestamps,
            user_requested_timezone=timezone,
            rounding_places=rounding_places,
            group_models=group_models,
            panels=panels,
        )
    if csv:
        from pvgisprototype.cli.write import write_solar_position_series_csv

        write_solar_position_series_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_position_series,
            position_parameters=solar_position_parameters,
            # user_requested_timestamps=timestamps,
            # user_requested_timezone=timezone,
            index=index,
            rounding_places=rounding_places,
            # group_models=group_models,
            filename=csv,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_solar_position_series

        # print(f'Input Solar position series : {solar_position_series}')
        uniplot_solar_position_series(
            solar_position_series=solar_position_series,
            position_parameters=solar_position_parameters,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            longitude=longitude,
            latitude=latitude,
            surface_orientation=True,
            surface_tilt=True,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Solar Position Series",
            title="Solar Position",
            label="Incidence",  # Review Me !
            legend_labels=None,
            terminal_width_fraction=terminal_width_fraction,
            verbose=verbose,
        )
    if horizon_plot:
        from pvgisprototype.cli.plot.horizon import plot_horizon_profile_x
        from numpy import degrees

        # Check the unit of the horizon_profile
        unit = horizon_profile.attrs.get('units', None)  # Adjust the attribute name as necessary

        # Convert to degrees if the unit is in radians
        if unit == 'radians':
            horizon_profile = degrees(horizon_profile)
        else:
            raise ValueError(f"Unknown unit for horizon_profile: {unit}")
        plot_horizon_profile_x(
                solar_position_series=solar_position_series,
                horizon_profile=horizon_profile,
                labels=["Horizontal plane", "Horizon height", "Solar altitude"],
                # colors=["cyan", "magenta", "yellow"],  # uncomment to override default
        )

position

Important sun and solar surface position parameters in calculating the amount of solar radiation that reaches a particular location on the Earth's surface

Functions:

Name Description
main

Solar position algorithms

main

main(
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    debug: Annotated[
        bool, Option(--debug, help="Enable debug mode")
    ] = False,
)

Solar position algorithms

Source code in pvgisprototype/cli/position/position.py
@app.callback()
def main(
    # ctx: typer.Context,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    debug: Annotated[
        bool, typer.Option("--debug", help="Enable debug mode")
    ] = False,
):
    """
    Solar position algorithms
    """
    # if verbose > 2:
    #     print(f"Executing command: {ctx.invoked_subcommand}")
    if verbose > 0:
        print("Will output verbosely")
        # state["verbose"] = True

    app.debug_mode = debug

shading

CLI module to calculate and overview the solar position parameters over a location for a period in time.

Functions:

Name Description
in_shade

in_shade

in_shade(
    ctx: Context,
    horizon_profile: Annotated[
        str | None, typer_argument_horizon_profile
    ],
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    random_surface_orientation: Annotated[
        bool, typer_option_random_surface_orientation
    ] = False,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    random_surface_tilt: Annotated[
        bool, typer_option_random_surface_tilt
    ] = False,
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = TIMEZONE_DEFAULT,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    shading_model: Annotated[
        List[ShadingModel], typer_option_shading_model
    ] = [pvgis],
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    position_parameter: Annotated[
        List[SolarPositionParameter],
        typer_option_solar_position_parameter,
    ] = [all],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = milne,
    solar_position_model: Annotated[
        SolarPositionModel,
        typer_option_solar_position_model,
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    complementary_incidence_angle: Annotated[
        bool,
        typer_option_sun_to_surface_plane_incidence_angle,
    ] = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: Annotated[
        bool,
        typer_option_zero_negative_solar_incidence_angle,
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool,
        "Visually cluster time series results per model",
    ] = False,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    panels: Annotated[
        bool, typer_option_panels_output
    ] = False,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
) -> None
Source code in pvgisprototype/cli/position/shading.py
@log_function_call
def in_shade(
    ctx: typer.Context,
    horizon_profile: Annotated[str | None, typer_argument_horizon_profile],
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    random_surface_orientation: Annotated[
        bool, typer_option_random_surface_orientation
    ] = False,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    random_surface_tilt: Annotated[
        bool, typer_option_random_surface_tilt
    ] = False,
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = TIMEZONE_DEFAULT,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    shading_model: Annotated[
        List[ShadingModel], typer_option_shading_model] = [ShadingModel.pvgis],
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    position_parameter: Annotated[
        List[SolarPositionParameter], typer_option_solar_position_parameter
    ] = [SolarPositionParameter.all],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SolarTimeModel.milne,
    solar_position_model: Annotated[
        SolarPositionModel, typer_option_solar_position_model
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    # solar_incidence_model: Annotated[SolarIncidenceModel, typer_option_solar_incidence_model] = SolarIncidenceModel.iqbal,
    complementary_incidence_angle: Annotated[
        bool, typer_option_sun_to_surface_plane_incidence_angle
    ] = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: Annotated[
        bool, typer_option_zero_negative_solar_incidence_angle
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool, "Visually cluster time series results per model"
    ] = False,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    panels: Annotated[bool, typer_option_panels_output] = False,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
) -> None:
    """ """
    # pass
    utc_timestamps = convert_timestamps_to_utc(
        user_requested_timezone=timezone,
        user_requested_timestamps=timestamps,
    )
    # Why does the callback function `_parse_model` not work?
    shading_models = select_models(
        ShadingModel, shading_model
    )  # Using a callback fails!

    surface_in_shade_series = calculate_surface_in_shade_series(
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        timezone=timezone,
        horizon_profile=horizon_profile,
        shading_models=shading_models,
        solar_time_model=solar_time_model,
        solar_position_model=solar_position_model,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        # unrefracted_solar_zenith=unrefracted_solar_zenith,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        angle_output_units=angle_output_units,
        dtype=dtype,
        array_backend=array_backend,
        verbose=verbose,
        log=log,
        validate_output=validate_output,
    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)

    # position_parameter = ctx.params.get(
    #     "position_parameter"
    # )  # Bug in Typer that is not passing correctly whatever is in the context ?

    # solar_position_parameters = select_models(
    #     SolarPositionParameter, position_parameter
    # )  # Using a callback fails!

    if not quiet:
        from pvgisprototype.cli.print.position.data import print_solar_position_series_table

        print_solar_position_series_table(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=surface_in_shade_series,
            position_parameters=[SolarPositionParameter.horizon, SolarPositionParameter.visible],
            title="Surface in shade",
            index=index,
            surface_orientation=False,
            surface_tilt=False,
            incidence=False,
            user_requested_timestamps=timestamps,
            user_requested_timezone=timezone,
            rounding_places=rounding_places,
            group_models=group_models,
            panels=panels,
        )
    # if csv:
    #     from pvgisprototype.cli.write import write_solar_position_series_csv

    #     write_solar_position_series_csv(
    #         longitude=longitude,
    #         latitude=latitude,
    #         timestamps=utc_timestamps,
    #         timezone=utc_timestamps.tz,
    #         table=shade_series,
    #         timing=True,
    #         declination=True,
    #         hour_angle=True,
    #         zenith=True,
    #         altitude=True,
    #         azimuth=True,
    #         surface_orientation=True,
    #         surface_tilt=True,
    #         incidence=True,
    #         user_requested_timestamps=timestamps,
    #         user_requested_timezone=timezone,
    #         # rounding_places=rounding_places,
    #         # group_models=group_models,
    #         filename=csv,
    #     )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_solar_position_series

        uniplot_solar_position_series(
            solar_position_series=surface_in_shade_series,
            position_parameters=[SolarPositionParameter.visible],
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            longitude=longitude,
            latitude=latitude,
            surface_orientation=True,
            surface_tilt=True,
            convert_false_to_none=False,  # `False` plots histogram of "In-Shade"
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Surface in Shade Series",
            title="Surface in Shade",
            label="Shade",
            legend_labels=None,
            terminal_width_fraction=terminal_width_fraction,
            verbose=verbose,
        )

zenith

CLI module to calculate the solar zenith angle for a location and a single moment in time.

Functions:

Name Description
zenith

Calculate the solar zenith angle

zenith

zenith(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        str | None, typer_option_timezone
    ] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    model: Annotated[
        List[SolarPositionModel],
        typer_option_solar_position_model,
    ] = [noaa],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool,
        "Visually cluster time series results per model",
    ] = False,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    panels: Annotated[
        bool, typer_option_panels_output
    ] = False,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
) -> None

Calculate the solar zenith angle

The solar zenith angle (SZA) is the angle between the zenith (directly overhead) and the line to the sun. A zenith angle of 0 degrees means the sun is directly overhead, while an angle of 90 degrees means the sun is on the horizon.

Parameters:

Name Type Description Default
Returns
required
Source code in pvgisprototype/cli/position/zenith.py
@log_function_call
def zenith(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[str | None, typer_option_timezone] = TIMEZONE_DEFAULT,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    model: Annotated[List[SolarPositionModel], typer_option_solar_position_model] = [
        SolarPositionModel.noaa
    ],
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    group_models: Annotated[
        bool, "Visually cluster time series results per model"
    ] = False,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    panels: Annotated[bool, typer_option_panels_output] = False,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
) -> None:
    """Calculate the solar zenith angle

    The solar zenith angle (SZA) is the angle between the zenith (directly
    overhead) and the line to the sun. A zenith angle of 0 degrees means the
    sun is directly overhead, while an angle of 90 degrees means the sun is on
    the horizon.

    Parameters
    ----------

    Returns
    -------

    """
    utc_timestamps = convert_timestamps_to_utc(
        user_requested_timezone=timezone,
        user_requested_timestamps=timestamps,
    )
    # Why does the callback function `_parse_model` not work?
    solar_position_models = select_models(
        SolarPositionModel, model
    )  # Using a callback fails!
    solar_zenith_series = calculate_solar_zenith_series(
        longitude=longitude,
        latitude=latitude,
        timestamps=utc_timestamps,
        timezone=utc_timestamps.tz,
        solar_position_models=solar_position_models,
        # solar_time_model=solar_time_model,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        angle_output_units=angle_output_units,
        array_backend=array_backend,
        dtype=dtype,
        verbose=verbose,
        validate_output=validate_output,
    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if not quiet:
        from pvgisprototype.cli.print.position.data import print_solar_position_series_table

        print_solar_position_series_table(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_zenith_series,
            position_parameters=[SolarPositionParameter.zenith],
            title="Solar Zenith Series",
            index=index,
            surface_orientation=None,
            surface_tilt=None,
            incidence=None,
            user_requested_timestamps=timestamps,
            user_requested_timezone=timezone,
            rounding_places=rounding_places,
            group_models=group_models,
            panels=panels,
        )
    if csv:
        from pvgisprototype.cli.write import write_solar_position_series_csv

        write_solar_position_series_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            table=solar_zenith_series,
            position_parameters=solar_position_parameters,
            # user_requested_timestamps=timestamps,
            # user_requested_timezone=timezone,
            index=index,
            rounding_places=rounding_places,
            # group_models=group_models,
            filename=csv,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_solar_position_series

        uniplot_solar_position_series(
            solar_position_series=solar_zenith_series,
            position_parameters=[SolarPositionParameter.zenith],
            timestamps=utc_timestamps,
            timezone=utc_timestamps.tz,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Solar Zenith Series",
            title="Solar Zenith",
            label="Zenith",
            legend_labels=None,
            terminal_width_fraction=terminal_width_fraction,
        )

power

Modules:

Name Description
average_photon_energy
broadband

CLI module to calculate the photovoltaic power output over a

broadband_multiple_surfaces

CLI module to calculate the photovoltaic power output over a

broadband_rear_side

CLI module to calculate the photovoltaic power output over a

energy_

energy

introduction
power
spectral
temperature

average_photon_energy

Functions:

Name Description
average_photon_energy

The Average Photon Energy (APE) characterises the energetic distribution

average_photon_energy

average_photon_energy(
    global_irradiance_series_up_to_1050,
    photon_flux_density,
    electron_charge=ELECTRON_CHARGE,
)

The Average Photon Energy (APE) characterises the energetic distribution in an irradiance spectrum. It is calculated by dividing the irradiance [W/m² or eV/m²/sec] by the photon flux density [number of photons/m²/sec]. [1]_

References

.. [1] Jardine, C.N. & Gottschalg, Ralph & Betts, Thomas & Infield, David. (2002). Influence of Spectral Effects on the Performance of Multijunction Amorphous Silicon Cells. to be published.

Source code in pvgisprototype/cli/power/average_photon_energy.py
@log_function_call
def average_photon_energy(  # series ?
    global_irradiance_series_up_to_1050,
    photon_flux_density,  # number_of_photons_up_to_1050 ?
    electron_charge=ELECTRON_CHARGE,
):
    """
    The Average Photon Energy (APE) characterises the energetic distribution
    in an irradiance spectrum. It is calculated by dividing the irradiance
    [W/m² or eV/m²/sec] by the photon flux density [number of photons/m²/sec].
    [1]_

    References
    ----------
    .. [1] Jardine, C.N. & Gottschalg, Ralph & Betts, Thomas & Infield, David.
      (2002). Influence of Spectral Effects on the Performance of Multijunction
      Amorphous Silicon Cells. to be published.
    """
    average_photon_energy = calculate_average_photon_energy(
        global_power_1050=global_irradiance_series_up_to_1050,
        photon_flux_density=photon_flux_density,
        electron_charge=ELECTRON_CHARGE,
    )

    print(average_photon_energy)

broadband

CLI module to calculate the photovoltaic power output over a location for a period in time.

Functions:

Name Description
photovoltaic_power_output_series

Estimate the photovoltaic power output for a location and a moment or period

photovoltaic_power_output_series

photovoltaic_power_output_series(
    ctx: Context,
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[
        DatetimeIndex | None, typer_argument_timestamps
    ] = str(now()),
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = None,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    photovoltaic_module_type: Annotated[
        PhotovoltaicModuleType,
        typer_option_photovoltaic_module_type,
    ] = Monofacial,
    global_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_global_horizontal_irradiance,
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_direct_horizontal_irradiance,
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries,
        typer_argument_spectral_factor_series,
    ] = SPECTRAL_FACTOR_DEFAULT,
    temperature_series: Annotated[
        TemperatureSeries, typer_option_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_option_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor,
        typer_option_linke_turbidity_factor_series,
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    albedo: Annotated[
        float | None, typer_option_albedo
    ] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel,
        typer_option_solar_position_model,
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    sun_horizon_position: Annotated[
        List[SunHorizonPositionModel],
        typer_option_sun_horizon_position,
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel,
        typer_option_solar_incidence_model,
    ] = iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool,
        typer_option_zero_negative_solar_incidence_angle,
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[
        float, typer_option_solar_constant
    ] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    horizon_profile: Annotated[
        DataArray | None, typer_option_horizon_profile
    ] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model
    ] = pvgis,
    shading_states: Annotated[
        List[ShadingState], typer_option_shading_state
    ] = [all],
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel,
        typer_option_photovoltaic_module_model,
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,
    peak_power: Annotated[
        float, typer_option_photovoltaic_module_peak_power
    ] = 1,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel,
        typer_option_pv_power_algorithm,
    ] = king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm,
        typer_option_module_temperature_algorithm,
    ] = faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = RADIANS,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    nomenclature: Annotated[
        bool, typer_option_nomenclature
    ] = NOMENCLATURE_FLAG_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = METADATA_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = NoneValue,
    profile: Annotated[
        bool, typer_option_profiling
    ] = cPROFILE_FLAG_DEFAULT,
)

Estimate the photovoltaic power output for a location and a moment or period in time.

Estimate the photovoltaic power over a time series or an arbitrarily aggregated energy production of a PV system connected to the electricity grid (without battery storage) based on broadband solar irradiance, ambient temperature and wind speed.

Notes

The optional input parameters global_horizontal_irradiance and direct_horizontal_irradiance accept any Xarray-support data file format and mean the global and direct irradiance on the horizontal plane.

Inside the API, however, and for legibility, the same parameters in the functions that calculate the diffuse and direct components, are defined as global_horizontal_component and direct_horizontal_component. This is to avoid confusion at the function level. For example, the function calculate_diffuse_inclined_irradiance_series() can read the direct horizontal component (thus the name of it direct_horizontal_component as well as simulate it. The point is to make it clear that if the direct_horizontal_component parameter is True (which means the user has provided an external dataset), then read it using the select_time_series() function.

Source code in pvgisprototype/cli/power/broadband.py
@log_function_call
def photovoltaic_power_output_series(
    ctx: typer.Context,
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[DatetimeIndex | None, typer_argument_timestamps] = str(
        Timestamp.now()
    ),
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = None,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    photovoltaic_module_type: Annotated[
        PhotovoltaicModuleType, typer_option_photovoltaic_module_type
    ] = PhotovoltaicModuleType.Monofacial,
    global_horizontal_irradiance: Annotated[
        Path | None, typer_option_global_horizontal_irradiance
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None, typer_option_direct_horizontal_irradiance
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries, typer_argument_spectral_factor_series
    ] = SPECTRAL_FACTOR_DEFAULT,  # Accept also list of float values ?
    temperature_series: Annotated[
        TemperatureSeries, typer_option_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_option_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor, typer_option_linke_turbidity_factor_series
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    # refracted_solar_zenith: Annotated[
    #     float | None, typer_option_refracted_solar_zenith
    # ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[float | None, typer_option_albedo] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel, typer_option_solar_position_model
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    sun_horizon_position: Annotated[
        List[SunHorizonPositionModel], typer_option_sun_horizon_position
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel, typer_option_solar_incidence_model
    ] = SolarIncidenceModel.iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool, typer_option_zero_negative_solar_incidence_angle
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[float, typer_option_solar_constant] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    horizon_profile: Annotated[DataArray | None, typer_option_horizon_profile] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model
    ] = ShadingModel.pvgis,  # for performance analysis : should be one !
    shading_states: Annotated[
        List[ShadingState], typer_option_shading_state
    ] = [ShadingState.all],
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel, typer_option_photovoltaic_module_model
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,  # PhotovoltaicModuleModel.CSI_FREE_STANDING,
    peak_power: Annotated[float, typer_option_photovoltaic_module_peak_power] = 1,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel, typer_option_pv_power_algorithm
    ] = PhotovoltaicModulePerformanceModel.king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm, typer_option_module_temperature_algorithm
    ] = ModuleTemperatureAlgorithm.faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    angle_output_units: Annotated[str, typer_option_angle_output_units] = RADIANS,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    nomenclature: Annotated[
        bool, typer_option_nomenclature
    ] = NOMENCLATURE_FLAG_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = METADATA_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = QuickResponseCode.NoneValue,
    profile: Annotated[bool, typer_option_profiling] = cPROFILE_FLAG_DEFAULT,
):
    """Estimate the photovoltaic power output for a location and a moment or period
    in time.

    Estimate the photovoltaic power over a time series or an arbitrarily
    aggregated energy production of a PV system connected to the electricity
    grid (without battery storage) based on broadband solar irradiance, ambient
    temperature and wind speed.

    Notes
    -----
    The optional input parameters `global_horizontal_irradiance` and
    `direct_horizontal_irradiance` accept any Xarray-support data file format
    and mean the global and direct irradiance on the horizontal plane.

    Inside the API, however, and for legibility, the same parameters in the
    functions that calculate the diffuse and direct components, are defined as
    `global_horizontal_component` and `direct_horizontal_component`. This is to
    avoid confusion at the function level. For example, the function
    `calculate_diffuse_inclined_irradiance_series()` can read the direct
    horizontal component (thus the name of it `direct_horizontal_component` as
    well as simulate it.  The point is to make it clear that if the
    `direct_horizontal_component` parameter is True (which means the user has
    provided an external dataset), then read it using the
    `select_time_series()` function.

    """
    # if global_horizontal_irradiance + direct_horizontal_irradiance are Path objects:
    if isinstance(global_horizontal_irradiance, (str, Path)) and isinstance(
        direct_horizontal_irradiance, (str, Path)
    ):  # NOTE This is in the case everything is pathlike
        global_horizontal_irradiance, direct_horizontal_irradiance = (
            read_horizontal_irradiance_components_from_sarah(
                shortwave=global_horizontal_irradiance,
                direct=direct_horizontal_irradiance,
                longitude=convert_float_to_degrees_if_requested(longitude, DEGREES),
                latitude=convert_float_to_degrees_if_requested(latitude, DEGREES),
                timestamps=timestamps,
                neighbor_lookup=neighbor_lookup,
                tolerance=tolerance,
                mask_and_scale=mask_and_scale,
                in_memory=in_memory,
                multi_thread=multi_thread,
                # multi_thread=False,
                verbose=verbose,
                log=log,
            )
        )
    temperature_series, wind_speed_series, spectral_factor_series = get_time_series(
        temperature_series=temperature_series,
        wind_speed_series=wind_speed_series,
        spectral_factor_series=spectral_factor_series,
        longitude=Longitude(value=longitude, unit='radians'),
        latitude=Latitude(values=latitude, units='radians'),
        timestamps=timestamps,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        in_memory=in_memory,
        dtype=dtype,
        array_backend=array_backend,
        multi_thread=multi_thread,
        verbose=verbose,
        log=log,
    )
    photovoltaic_power_output_series = calculate_photovoltaic_power_output_series(
        longitude=longitude,
        latitude=latitude,
        elevation=elevation,
        surface_orientation=surface_orientation,
        surface_tilt=surface_tilt,
        timestamps=timestamps,
        timezone=timezone,
        global_horizontal_irradiance=global_horizontal_irradiance,
        direct_horizontal_irradiance=direct_horizontal_irradiance,
        spectral_factor_series=spectral_factor_series,
        temperature_series=temperature_series,
        wind_speed_series=wind_speed_series,
        linke_turbidity_factor_series=linke_turbidity_factor_series,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        # unrefracted_solar_zenith=unrefracted_solar_zenith,
        albedo=albedo,
        apply_reflectivity_factor=apply_reflectivity_factor,
        solar_position_model=solar_position_model,
        sun_horizon_position=sun_horizon_position,
        solar_incidence_model=solar_incidence_model,
        zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
        solar_time_model=solar_time_model,
        solar_constant=solar_constant,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        horizon_profile=horizon_profile,  # Review naming please ?
        shading_model=shading_model,
        shading_states=shading_states,
        # angle_output_units=angle_output_units,
        photovoltaic_module_type=photovoltaic_module_type,
        photovoltaic_module=photovoltaic_module,
        peak_power=peak_power,
        system_efficiency=system_efficiency,
        power_model=power_model,
        temperature_model=temperature_model,
        efficiency=efficiency,
        dtype=dtype,
        array_backend=array_backend,
        # multi_thread=multi_thread,
        verbose=verbose,
        log=log,
        fingerprint=fingerprint,
        profile=profile,
        validate_output=validate_output,
    )  # Re-Design Me ! ------------------------------------------------

    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)

    if quick_response_code.value != QuickResponseCode.NoneValue:
        from pvgisprototype.cli.print.qr import print_quick_response_code

        print_quick_response_code(
            dictionary=photovoltaic_power_output_series.output,
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            surface_orientation=True,
            surface_tilt=True,
            timestamps=timestamps,
            rounding_places=rounding_places,
            output_type=quick_response_code,
        )
        return
    if not quiet:
        if verbose > 0:
            from pvgisprototype.cli.print.irradiance.data import print_irradiance_table_2

            print_irradiance_table_2(
                title=photovoltaic_power_output_series.title + f" series [{POWER_UNIT}]",
                irradiance_data=photovoltaic_power_output_series.output,
                # rear_side_irradiance_data=rear_side_photovoltaic_power_output_series.output if rear_side_photovoltaic_power_output_series else None,
                longitude=longitude,
                latitude=latitude,
                elevation=elevation,
                timestamps=timestamps,
                timezone=timezone,
                rounding_places=rounding_places,
                index=index,
                verbose=verbose,
            )
        else:
            # # Redesign Me : Handle this "upstream", avoid alterations here ?
            # if photovoltaic_module_type == PhotovoltaicModuleType.Bifacial:
            #     photovoltaic_power_output_series.value += (
            #         rear_side_photovoltaic_power_output_series.value
            #     )
            # # ------------------------- Better handling of rounding vs dtype ?
            print(
                ",".join(
                    round_float_values(
                        photovoltaic_power_output_series.value.flatten(),
                        rounding_places,
                    ).astype(str)
                    # photovoltaic_power_output_series.value.flatten().astype(str)
                )
            )
    if statistics:
        from pvgisprototype.cli.print.series import print_series_statistics

        print_series_statistics(
            data_array=photovoltaic_power_output_series.value,
            timestamps=timestamps,
            groupby=groupby,
            title=photovoltaic_power_output_series.title,
            rounding_places=rounding_places,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_data_array_series

        extra_data_array = (
            [rear_side_photovoltaic_power_output_series.value]
            if "rear_side_photovoltaic_power_output_series" in locals()
            # and rear_side_photovoltaic_power_output_series.value is not None
            and rear_side_photovoltaic_power_output_series is not None
            else []
        )
        orientation = (
            [surface_orientation, rear_side_surface_orientation]
            if "rear_side_surface_orientation" in locals()
            else [surface_orientation]
        )
        tilt = (
            [surface_tilt, rear_side_surface_tilt]
            if "rear_side_surface_tilt" in locals()
            else [surface_tilt]
        )
        uniplot_data_array_series(
            data_array=photovoltaic_power_output_series.value,
            # list_extra_data_arrays=extra_data_array,
            longitude=longitude,
            latitude=latitude,
            orientation=orientation,  # [surface_orientation, rear_side_surface_orientation],
            tilt=tilt,  # [surface_tilt, rear_side_surface_tilt],
            timestamps=timestamps,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle=photovoltaic_power_output_series.supertitle,
            title=photovoltaic_power_output_series.title,
            label=photovoltaic_power_output_series.label,
            # extra_legend_labels=["Rear-side Photovoltaic Power"],
            unit=POWER_UNIT,
            terminal_width_fraction=terminal_width_fraction,
        )
    if metadata:
        import click

        from pvgisprototype.cli.print.metadata import print_command_metadata

        print_command_metadata(context=click.get_current_context())
    if fingerprint:
        from pvgisprototype.cli.print.fingerprint import print_finger_hash

        print_finger_hash(dictionary=photovoltaic_power_output_series.output)
    # Call write_irradiance_csv() last : it modifies the input dictionary !
    if csv:
        from pvgisprototype.cli.write import write_irradiance_csv

        write_irradiance_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dictionary=photovoltaic_power_output_series.output,
            filename=csv,
            index=index,
        )

broadband_multiple_surfaces

CLI module to calculate the photovoltaic power output over a location for a period in time.

Functions:

Name Description
photovoltaic_power_output_series_from_multiple_surfaces

Estimate the sum of photovoltaic output for multiple solar surface

photovoltaic_power_output_series_from_multiple_surfaces

photovoltaic_power_output_series_from_multiple_surfaces(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        list | None, typer_option_surface_orientation_multi
    ] = [float(SURFACE_ORIENTATION_DEFAULT)],
    surface_tilt: Annotated[
        list | None, typer_option_surface_tilt_multi
    ] = [float(SURFACE_TILT_DEFAULT)],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now()),
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = None,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    global_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_global_horizontal_irradiance,
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_direct_horizontal_irradiance,
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries,
        typer_argument_spectral_factor_series,
    ] = SPECTRAL_FACTOR_DEFAULT,
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor,
        typer_option_linke_turbidity_factor_series,
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[
        float | None, typer_option_albedo
    ] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel,
        typer_option_solar_position_model,
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    sun_horizon_position: Annotated[
        List[SunHorizonPositionModel],
        typer_option_sun_horizon_position,
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel,
        typer_option_solar_incidence_model,
    ] = iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool,
        typer_option_zero_negative_solar_incidence_angle,
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    horizon_profile: Annotated[
        DataArray | None, typer_option_horizon_profile
    ] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model
    ] = pvgis,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[
        float, typer_option_solar_constant
    ] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = RADIANS,
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel,
        typer_option_photovoltaic_module_model,
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel,
        typer_option_pv_power_algorithm,
    ] = king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm,
        typer_option_module_temperature_algorithm,
    ] = faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = NoneValue,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
    profile: Annotated[
        bool, typer_option_profiling
    ] = cPROFILE_FLAG_DEFAULT,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
)

Estimate the sum of photovoltaic output for multiple solar surface setups for a location and a moment or period in time.

Estimate the sum of photovoltaic power over a time series or an arbitrarily aggregated energy production of multiple PV system setups, i.e. different tilts and orientations, connected to the electricity grid (without battery storage) based on broadband solar irradiance, ambient temperature and wind speed.

Notes

The optional input parameters global_horizontal_irradiance and direct_horizontal_irradiance accept any Xarray-support data file format and mean the global and direct irradiance on the horizontal plane.

Inside the API, however, and for legibility, the same parameters in the functions that calculate the diffuse and direct components, are defined as global_horizontal_component and direct_horizontal_component. This is to avoid confusion at the function level. For example, the function calculate_diffuse_inclined_irradiance_series() can read the direct horizontal component (thus the name of it direct_horizontal_component as well as simulate it. The point is to make it clear that if the direct_horizontal_component parameter is True (which means the user has provided an external dataset), then read it using the select_time_series() function.

Source code in pvgisprototype/cli/power/broadband_multiple_surfaces.py
@log_function_call
def photovoltaic_power_output_series_from_multiple_surfaces(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        list | None, typer_option_surface_orientation_multi
    ] = [float(SURFACE_ORIENTATION_DEFAULT)],
    surface_tilt: Annotated[list | None, typer_option_surface_tilt_multi] = [
        float(SURFACE_TILT_DEFAULT)
    ],
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(Timestamp.now()),
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = None,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    global_horizontal_irradiance: Annotated[
        Path | None, typer_option_global_horizontal_irradiance
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None, typer_option_direct_horizontal_irradiance
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries, typer_argument_spectral_factor_series
    ] = SPECTRAL_FACTOR_DEFAULT,  # Accept also list of float values ?
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor, typer_option_linke_turbidity_factor_series
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[float | None, typer_option_albedo] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel, typer_option_solar_position_model
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    sun_horizon_position: Annotated[
        List[SunHorizonPositionModel], typer_option_sun_horizon_position
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel, typer_option_solar_incidence_model
    ] = SolarIncidenceModel.iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool, typer_option_zero_negative_solar_incidence_angle
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    horizon_profile: Annotated[DataArray | None, typer_option_horizon_profile] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model] = ShadingModel.pvgis,  # for performance analysis : should be one !
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[float, typer_option_solar_constant] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    angle_output_units: Annotated[str, typer_option_angle_output_units] = RADIANS,
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel, typer_option_photovoltaic_module_model
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,  # PhotovoltaicModuleModel.CSI_FREE_STANDING,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel, typer_option_pv_power_algorithm
    ] = PhotovoltaicModulePerformanceModel.king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm, typer_option_module_temperature_algorithm
    ] = ModuleTemperatureAlgorithm.faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = QuickResponseCode.NoneValue,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
    profile: Annotated[bool, typer_option_profiling] = cPROFILE_FLAG_DEFAULT,
    validate_output: Annotated[bool, typer_option_validate_output] = VALIDATE_OUTPUT_DEFAULT,
):
    """Estimate the sum of photovoltaic output for multiple solar surface
    setups for a location and a moment or period in time.

    Estimate the sum of photovoltaic power over a time series or an arbitrarily
    aggregated energy production of multiple PV system setups, i.e. different
    tilts and orientations, connected to the electricity grid (without battery
    storage) based on broadband solar irradiance, ambient temperature and wind
    speed.

    Notes
    -----
    The optional input parameters `global_horizontal_irradiance` and
    `direct_horizontal_irradiance` accept any Xarray-support data file format
    and mean the global and direct irradiance on the horizontal plane.

    Inside the API, however, and for legibility, the same parameters in the
    functions that calculate the diffuse and direct components, are defined as
    `global_horizontal_component` and `direct_horizontal_component`. This is to
    avoid confusion at the function level. For example, the function
    `calculate_diffuse_inclined_irradiance_series()` can read the direct
    horizontal component (thus the name of it `direct_horizontal_component` as
    well as simulate it.  The point is to make it clear that if the
    `direct_horizontal_component` parameter is True (which means the user has
    provided an external dataset), then read it using the
    `select_time_series()` function.

    """
    if len(surface_tilt) != len(surface_orientation):
        from pvgisprototype.api.series.hardcodings import exclamation_mark

        logger.error(
            f"{exclamation_mark} Aborting as length of --surface_orientation and --surface_tilt is not the same!",
            alt=f"{exclamation_mark} [red]Aborting[/red] as [red]length[/red] [code]--surface-orientation[/code] and [code]--surface-tilt[/code] [red]is not the same[/red]!",
        )
        return
    if isinstance(global_horizontal_irradiance, (str, Path)) and isinstance(
        direct_horizontal_irradiance, (str, Path)
    ):  # NOTE This is in the case everything is pathlike
        horizontal_irradiance_components = (
            read_horizontal_irradiance_components_from_sarah(
                shortwave=global_horizontal_irradiance,
                direct=direct_horizontal_irradiance,
                longitude=convert_float_to_degrees_if_requested(longitude, DEGREES),
                latitude=convert_float_to_degrees_if_requested(latitude, DEGREES),
                timestamps=timestamps,
                neighbor_lookup=neighbor_lookup,
                tolerance=tolerance,
                mask_and_scale=mask_and_scale,
                in_memory=in_memory,
                multi_thread=multi_thread,
                # multi_thread=False,
                verbose=verbose,
                log=log,
            )
        )
        global_horizontal_irradiance = horizontal_irradiance_components[
            GLOBAL_HORIZONTAL_IRRADIANCE_COLUMN_NAME
        ]
        direct_horizontal_irradiance = horizontal_irradiance_components[
            DIRECT_HORIZONTAL_IRRADIANCE_COLUMN_NAME
        ]
    temperature_series, wind_speed_series, spectral_factor_series = get_time_series(
        temperature_series=temperature_series,
        wind_speed_series=wind_speed_series,
        spectral_factor_series=spectral_factor_series,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        in_memory=in_memory,
        dtype=dtype,
        array_backend=array_backend,
        multi_thread=multi_thread,
        verbose=verbose,
        log=log,
    )
    photovoltaic_power_output_series = calculate_photovoltaic_power_output_series_from_multiple_surfaces(
        longitude=longitude,
        latitude=latitude,
        elevation=elevation,
        surface_orientations=surface_orientation,
        surface_tilts=surface_tilt,
        timestamps=timestamps,
        timezone=timezone,
        global_horizontal_irradiance=global_horizontal_irradiance,
        direct_horizontal_irradiance=direct_horizontal_irradiance,
        spectral_factor_series=spectral_factor_series,
        temperature_series=temperature_series,
        wind_speed_series=wind_speed_series,
        dtype=dtype,
        array_backend=array_backend,
        multi_thread=multi_thread,
        linke_turbidity_factor_series=linke_turbidity_factor_series,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        # unrefracted_solar_zenith=unrefracted_solar_zenith,
        albedo=albedo,
        apply_reflectivity_factor=apply_reflectivity_factor,
        solar_position_model=solar_position_model,
        sun_horizon_position=sun_horizon_position,
        solar_incidence_model=solar_incidence_model,
        zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
        solar_time_model=solar_time_model,
        solar_constant=solar_constant,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        angle_output_units=angle_output_units,
        horizon_profile=horizon_profile,
        shading_model=shading_model,
        photovoltaic_module=photovoltaic_module,
        system_efficiency=system_efficiency,
        power_model=power_model,
        temperature_model=temperature_model,
        efficiency=efficiency,
        verbose=verbose,
        log=log,
        fingerprint=fingerprint,
        profile=profile,
        validate_output=validate_output,
    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if quick_response_code.value != QuickResponseCode.NoneValue:
        from pvgisprototype.cli.print.qr import print_quick_response_code

        print_quick_response_code(
            dictionary=photovoltaic_power_output_series.output,
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            surface_orientation=True,
            surface_tilt=True,
            timestamps=timestamps,
            rounding_places=rounding_places,
        )
        return
    if not quiet:
        if verbose > 0:
            from pvgisprototype.cli.print.irradiance.data import print_irradiance_table_2

            print_irradiance_table_2(
                title=photovoltaic_power_output_series.title + f" series [{POWER_UNIT}]",
                irradiance_data=photovoltaic_power_output_series.output,
                longitude=longitude,
                latitude=latitude,
                elevation=elevation,
                timestamps=timestamps,
                timezone=timezone,
                rounding_places=rounding_places,
                index=index,
                verbose=verbose,
            )
        else:
            flat_list = photovoltaic_power_output_series.value.flatten().astype(str)
            csv_str = ",".join(flat_list)
            print(csv_str)
    if statistics:
        from pvgisprototype.cli.print.series import print_series_statistics

        print_series_statistics(
            data_array=photovoltaic_power_output_series.series,
            timestamps=timestamps,
            groupby=groupby,
            title="Photovoltaic power output",
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_data_array_series

        individual_series = [
            series.value
            for series in photovoltaic_power_output_series.individual_series
        ]
        surface_orientation = [
            convert_float_to_degrees_if_requested(orientation, angle_output_units)
            for orientation in surface_orientation
        ]
        surface_tilt = [
            convert_float_to_degrees_if_requested(tilt, angle_output_units)
            for tilt in surface_tilt
        ]
        surface_orientation = round_float_values(surface_orientation, rounding_places)
        surface_tilt = round_float_values(surface_tilt, rounding_places)
        individual_labels = [
            f"Orientation, Tilt : {orientation}°, {tilt}°"
            for orientation, tilt in zip(surface_orientation, surface_tilt)
        ]
        uniplot_data_array_series(
            data_array=photovoltaic_power_output_series.value,
            list_extra_data_arrays=individual_series,
            timestamps=timestamps,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Photovoltaic Power Output Series",
            title="Photovoltaic power output from multiple surfaces",
            label="Sum of Photovoltaic Power",
            extra_legend_labels=individual_labels,
            unit=POWER_UNIT,
            terminal_width_fraction=terminal_width_fraction,
        )
    if fingerprint:
        from pvgisprototype.cli.print.fingerprint import print_finger_hash

        print_finger_hash(dictionary=photovoltaic_power_output_series.output)
    if metadata:
        import click

        from pvgisprototype.cli.print.metadata import print_command_metadata

        print_command_metadata(context=click.get_current_context())
    # Call write_irradiance_csv() last : it modifies the input dictionary !
    if csv:
        from pvgisprototype.cli.write import write_irradiance_csv

        write_irradiance_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dictionary=photovoltaic_power_output_series.output,
            filename=csv,
            index=index,
        )

broadband_rear_side

CLI module to calculate the photovoltaic power output over a location for a period in time.

Functions:

Name Description
rear_side_photovoltaic_power_output_series

Estimate the photovoltaic power output for a location and a moment or period

rear_side_photovoltaic_power_output_series

rear_side_photovoltaic_power_output_series(
    ctx: Context,
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None,
        typer_argument_rear_side_surface_orientation,
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_argument_rear_side_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[
        DatetimeIndex | None, typer_argument_timestamps
    ] = str(now()),
    timezone: Annotated[
        ZoneInfo | None, typer_option_timezone
    ] = None,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    global_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_global_horizontal_irradiance,
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None,
        typer_option_direct_horizontal_irradiance,
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries,
        typer_argument_spectral_factor_series,
    ] = SPECTRAL_FACTOR_DEFAULT,
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor,
        typer_option_linke_turbidity_factor_series,
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[
        float | None, typer_option_albedo
    ] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel,
        typer_option_solar_position_model,
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    sun_horizon_position: Annotated[
        List[SunHorizonPositionModel],
        typer_option_sun_horizon_position,
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel,
        typer_option_solar_incidence_model,
    ] = iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool,
        typer_option_zero_negative_solar_incidence_angle,
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[
        float, typer_option_solar_constant
    ] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    horizon_profile: Annotated[
        DataArray | None, typer_option_horizon_profile
    ] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model
    ] = pvgis,
    shading_states: Annotated[
        List[ShadingState], typer_option_shading_state
    ] = SHADING_STATE_DEFAULT,
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel,
        typer_option_photovoltaic_module_model,
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,
    peak_power: Annotated[
        float, typer_option_photovoltaic_module_peak_power
    ] = 1,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel,
        typer_option_pv_power_algorithm,
    ] = king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm,
        typer_option_module_temperature_algorithm,
    ] = faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = REAR_SIDE_EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = RADIANS,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    nomenclature: Annotated[
        bool, typer_option_nomenclature
    ] = NOMENCLATURE_FLAG_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = METADATA_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = NoneValue,
    profile: Annotated[
        bool, typer_option_profiling
    ] = cPROFILE_FLAG_DEFAULT,
)

Estimate the photovoltaic power output for a location and a moment or period in time.

Estimate the photovoltaic power over a time series or an arbitrarily aggregated energy production of a PV system connected to the electricity grid (without battery storage) based on broadband solar irradiance, ambient temperature and wind speed.

Notes

The optional input parameters global_horizontal_irradiance and direct_horizontal_irradiance accept any Xarray-support data file format and mean the global and direct irradiance on the horizontal plane.

Inside the API, however, and for legibility, the same parameters in the functions that calculate the diffuse and direct components, are defined as global_horizontal_component and direct_horizontal_component. This is to avoid confusion at the function level. For example, the function calculate_diffuse_inclined_irradiance_series() can read the direct horizontal component (thus the name of it direct_horizontal_component as well as simulate it. The point is to make it clear that if the direct_horizontal_component parameter is True (which means the user has provided an external dataset), then read it using the select_time_series() function.

Source code in pvgisprototype/cli/power/broadband_rear_side.py
@log_function_call
def rear_side_photovoltaic_power_output_series(
    ctx: typer.Context,
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None, typer_argument_rear_side_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_argument_rear_side_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[DatetimeIndex | None, typer_argument_timestamps] = str(
        Timestamp.now()
    ),
    timezone: Annotated[ZoneInfo | None, typer_option_timezone] = None,
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    global_horizontal_irradiance: Annotated[
        Path | None, typer_option_global_horizontal_irradiance
    ] = None,
    direct_horizontal_irradiance: Annotated[
        Path | None, typer_option_direct_horizontal_irradiance
    ] = None,
    spectral_factor_series: Annotated[
        SpectralFactorSeries, typer_argument_spectral_factor_series
    ] = SPECTRAL_FACTOR_DEFAULT,  # Accept also list of float values ?
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor, typer_option_linke_turbidity_factor_series
    ] = LinkeTurbidityFactor(),
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[float | None, typer_option_albedo] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = ANGULAR_LOSS_FACTOR_FLAG_DEFAULT,
    solar_position_model: Annotated[
        SolarPositionModel, typer_option_solar_position_model
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    sun_horizon_position: Annotated[
        List[SunHorizonPositionModel], typer_option_sun_horizon_position
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel, typer_option_solar_incidence_model
    ] = SolarIncidenceModel.iqbal,
    zero_negative_solar_incidence_angle: Annotated[
        bool, typer_option_zero_negative_solar_incidence_angle
    ] = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[float, typer_option_solar_constant] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    horizon_profile: Annotated[DataArray | None, typer_option_horizon_profile] = None,
    shading_model: Annotated[
        ShadingModel, typer_option_shading_model
    ] = ShadingModel.pvgis,  # for performance analysis : should be one !
    shading_states: Annotated[
        List[ShadingState], typer_option_shading_state
    ] = SHADING_STATE_DEFAULT,
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel, typer_option_photovoltaic_module_model
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,  # PhotovoltaicModuleModel.CSI_FREE_STANDING,
    peak_power: Annotated[float, typer_option_photovoltaic_module_peak_power] = 1,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,  # REAR_SIDE_SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel, typer_option_pv_power_algorithm
    ] = PhotovoltaicModulePerformanceModel.king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm, typer_option_module_temperature_algorithm
    ] = ModuleTemperatureAlgorithm.faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = REAR_SIDE_EFFICIENCY_FACTOR_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    multi_thread: Annotated[
        bool, typer_option_multi_thread
    ] = MULTI_THREAD_FLAG_DEFAULT,
    angle_output_units: Annotated[str, typer_option_angle_output_units] = RADIANS,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    nomenclature: Annotated[
        bool, typer_option_nomenclature
    ] = NOMENCLATURE_FLAG_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    validate_output: Annotated[
        bool, typer_option_validate_output
    ] = VALIDATE_OUTPUT_DEFAULT,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = METADATA_FLAG_DEFAULT,
    quick_response_code: Annotated[
        QuickResponseCode, typer_option_quick_response
    ] = QuickResponseCode.NoneValue,
    profile: Annotated[bool, typer_option_profiling] = cPROFILE_FLAG_DEFAULT,
):
    """Estimate the photovoltaic power output for a location and a moment or period
    in time.

    Estimate the photovoltaic power over a time series or an arbitrarily
    aggregated energy production of a PV system connected to the electricity
    grid (without battery storage) based on broadband solar irradiance, ambient
    temperature and wind speed.

    Notes
    -----
    The optional input parameters `global_horizontal_irradiance` and
    `direct_horizontal_irradiance` accept any Xarray-support data file format
    and mean the global and direct irradiance on the horizontal plane.

    Inside the API, however, and for legibility, the same parameters in the
    functions that calculate the diffuse and direct components, are defined as
    `global_horizontal_component` and `direct_horizontal_component`. This is to
    avoid confusion at the function level. For example, the function
    `calculate_diffuse_inclined_irradiance_series()` can read the direct
    horizontal component (thus the name of it `direct_horizontal_component` as
    well as simulate it.  The point is to make it clear that if the
    `direct_horizontal_component` parameter is True (which means the user has
    provided an external dataset), then read it using the
    `select_time_series()` function.

    """
    temperature_series, wind_speed_series, spectral_factor_series = get_time_series(
        temperature_series=temperature_series,
        wind_speed_series=wind_speed_series,
        spectral_factor_series=spectral_factor_series,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        in_memory=in_memory,
        dtype=dtype,
        array_backend=array_backend,
        multi_thread=multi_thread,
        verbose=verbose,
        log=log,
    )
    rear_side_photovoltaic_power_output_series = calculate_rear_side_photovoltaic_power_output_series(
        longitude=longitude,
        latitude=latitude,
        elevation=elevation,
        rear_side_surface_orientation=surface_orientation,
        rear_side_surface_tilt=surface_tilt,
        timestamps=timestamps,
        timezone=timezone,
        global_horizontal_irradiance=global_horizontal_irradiance,
        direct_horizontal_irradiance=direct_horizontal_irradiance,
        spectral_factor_series=spectral_factor_series,
        temperature_series=temperature_series,
        wind_speed_series=wind_speed_series,
        linke_turbidity_factor_series=linke_turbidity_factor_series,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        albedo=albedo,
        apply_reflectivity_factor=apply_reflectivity_factor,
        solar_position_model=solar_position_model,
        solar_incidence_model=solar_incidence_model,
        zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
        solar_time_model=solar_time_model,
        solar_constant=solar_constant,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        horizon_profile=horizon_profile,  # Review naming please ?
        shading_model=shading_model,
        angle_output_units=angle_output_units,
        # photovoltaic_module_type=photovoltaic_module_type,
        photovoltaic_module=photovoltaic_module,
        peak_power=peak_power,
        system_efficiency=system_efficiency,
        power_model=power_model,
        temperature_model=temperature_model,
        rear_side_efficiency=efficiency,
        dtype=dtype,
        array_backend=array_backend,
        verbose=verbose,
        log=log,
        fingerprint=fingerprint,
        profile=profile,
        validate_output=validate_output,
    )  # Re-Design Me ! ------------------------------------------------
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if quick_response_code.value != QuickResponseCode.NoneValue:
        from pvgisprototype.cli.print.qr import print_quick_response_code

        print_quick_response_code(
            dictionary=rear_side_photovoltaic_power_output_series.components,
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            surface_orientation=True,
            surface_tilt=True,
            timestamps=timestamps,
            rounding_places=rounding_places,
            output_type=quick_response_code,
        )
        return
    if not quiet:
        if verbose > 0:
            from pvgisprototype.cli.print.irradiance import print_irradiance_table_2

            print_irradiance_table_2(
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                dictionary=rear_side_photovoltaic_power_output_series.components,
                # title=rear_side_photovoltaic_power_output_series['Title'] + f" series {POWER_UNIT}",
                rounding_places=rounding_places,
                index=index,
                surface_orientation=True,
                surface_tilt=True,
                verbose=verbose,
            )
        else:
            # ------------------------- Better handling of rounding vs dtype ?
            print(
                ",".join(
                    # round_float_values(
                    #     rear_side_photovoltaic_power_output_series.value.flatten(),
                    #     rounding_places,
                    # ).astype(str)
                    rear_side_photovoltaic_power_output_series.value.flatten().astype(
                        str
                    )
                )
            )
    if statistics:
        from pvgisprototype.cli.print.series import print_series_statistics

        print_series_statistics(
            data_array=rear_side_photovoltaic_power_output_series,
            timestamps=timestamps,
            groupby=groupby,
            title="Photovoltaic power output",
            rounding_places=rounding_places,
        )
    if uniplot:
        from pvgisprototype.api.plot import uniplot_data_array_series

        uniplot_data_array_series(
            data_array=rear_side_photovoltaic_power_output_series.value,
            timestamps=timestamps,
            resample_large_series=resample_large_series,
            lines=True,
            supertitle="Photovoltaic Power Output Series",
            title="Photovoltaic power output",
            label="Photovoltaic Power",
            extra_legend_labels=None,
            unit=POWER_UNIT,
            terminal_width_fraction=terminal_width_fraction,
        )
    if metadata:
        import click

        from pvgisprototype.cli.print.metadata import print_command_metadata

        print_command_metadata(context=click.get_current_context())
    if fingerprint:
        from pvgisprototype.cli.print.fingerprint import print_finger_hash

        print_finger_hash(
            dictionary=rear_side_photovoltaic_power_output_series.components
        )
    # Call write_irradiance_csv() last : it modifies the input dictionary !
    if csv:
        from pvgisprototype.cli.write import write_irradiance_csv

        write_irradiance_csv(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dictionary=rear_side_photovoltaic_power_output_series.components,
            filename=csv,
            index=index,
        )

energy_

energy grid - fixed - tracking off-grid

Functions:

Name Description
estimate_grid_connected_pv

Estimate the energy production of a PV system connected to the electricity grid

estimate_offgrid_pv

Estimate the energy production of a PV system that is not connected to

estimate_tracking_pv

Estimate the energy production of a tracking PV system connected to the electricity grid

estimate_grid_connected_pv

estimate_grid_connected_pv(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    peak_power: Annotated[
        float,
        Argument(
            rich_help_panel=Required,
            help="The installed peak PV power in kWp",
            min=0,
            max=100000000,
        ),
    ],
    loss: Annotated[
        float,
        Argument(
            rich_help_panel=Preset,
            help="System losses in %",
            min=0,
            max=100,
        ),
    ] = 14,
    solar_radiation_database: Annotated[
        str | None,
        Argument(
            rich_help_panel=Preset,
            help="Solar radiation database with hourly time resolution",
        ),
    ] = "PVGIS-SARAH2",
    consider_shadows: Annotated[
        bool,
        Argument(
            rich_help_panel=Preset,
            help="Calculate effect of horizon shadowing",
        ),
    ] = True,
    horizon_heights: Annotated[
        List[float], typer_argument_horizon_heights
    ] = None,
    pv_techonology: Annotated[
        str | None, typer_argument_pv_technology
    ] = None,
    mounting_type: Annotated[
        str | None, typer_argument_mounting_type
    ] = "free",
    surface_orientation: Annotated[
        float, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float, typer_argument_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    optimise_surface_tilt: Annotated[
        bool, typer_option_optimise_surface_tilt
    ] = OPTIMISE_SURFACE_TILT_FLAG_DEFAULT,
    optimise_surface_geometry: Annotated[
        bool, typer_option_optimise_surface_geometry
    ] = OPTIMISE_SURFACE_GEOMETRY_FLAG_DEFAULT,
    single_axis_system: Annotated[
        bool,
        Argument(
            rich_help_panel=Optional,
            help="Consider a single axis PV system -- Remove Me and improve single_axis_inclination!",
        ),
    ] = False,
    single_axis_inclination: Annotated[
        float,
        Argument(
            rich_help_panel=Optional,
            help="Inclination for a single axis PV system",
            min=0,
            max=90,
        ),
    ] = 0,
    optimise_single_axis_inclination: Annotated[
        bool,
        Argument(
            rich_help_panel=Optional,
            help="Optimise inclination for a single axis PV system",
        ),
    ] = False,
    vertical_axis_system: Annotated[
        bool,
        Argument(
            rich_help_panel=Optional,
            help="Consider a single vertical axis PV system -- Remove Me and improve vertical_axis_inclination!",
        ),
    ] = False,
    vertical_axis_inclination: Annotated[
        float,
        Argument(
            rich_help_panel=Optional,
            help="Inclination for a single axis PV system",
            min=0,
            max=90,
        ),
    ] = 0,
    optimise_vertical_axis_inclination: Annotated[
        bool,
        Argument(
            rich_help_panel=Optional,
            help="Optimise inclination for a single vertical axis PV system",
        ),
    ] = False,
    two_axis_system: Annotated[
        bool,
        Argument(
            rich_help_panel=Optional,
            help="Consider a two-axis tracking PV system -- Review Me!",
        ),
    ] = False,
    electricity_price: Annotated[
        bool,
        Argument(
            rich_help_panel=Optional,
            help="Calculate the PV electricity price (kwh/year) for the system cost in the user requested currency -- Review Me!",
        ),
    ] = False,
    cost: Annotated[
        float,
        Argument(
            rich_help_panel=Optional,
            help="Total cost of installing the PV system [custom currency]",
            min=0,
            max=100000000,
        ),
    ] = 0,
    interest: Annotated[
        float,
        Argument(
            rich_help_panel=Optional,
            help="Interest in %/year",
            min=0,
            max=100,
        ),
    ] = None,
    lifetime: Annotated[
        int,
        Argument(
            rich_help_panel=Optional,
            help="Expected lifetime of the PV system in years",
            min=0,
            max=100,
        ),
    ] = 25,
    output_format: Annotated[
        str | None, Argument(help="Output format")
    ] = "csv",
)

Estimate the energy production of a PV system connected to the electricity grid

Performance of grid-connected PV systems. This tool makes it possible to estimate the average monthly and yearly energy production of a PV system connected to the electricity grid, without battery storage. The calculation takes into account the solar radiation, temperature, wind speed and type of PV module. The user can choose how the modules are mounted, whether on a free-standing rack mounting, sun-tracking mountings or integrated in a building surface. PVGIS can also calculate the optimum slope and orientation that maximizes the yearly energy production.

Requires following data:

- elevation
- horizon
- dem_era5
- tgrad_bin
- sis
- sid
- temperature (ERA5 t2m)
- wind speed (ERA5 w2m)
- pv coefficients (current file: pvtech.coeffs)
- pv coefficients for bifacial? (current file: pvtech.coeffs_bipv)
- spectral correction data (current file: pvtech_spectraldata.bin)

Parameters:

Name Type Description Default
longitude Annotated[float, typer_argument_longitude]
required
latitude Annotated[float, typer_argument_latitude]
required
peak_power Annotated[float, Argument(rich_help_panel=Required, help='The installed peak PV power in kWp', min=0, max=100000000)]
required
loss Annotated[float, Argument(rich_help_panel=Preset, help='System losses in %', min=0, max=100)]
14
solar_radiation_database Annotated[str | None, Argument(rich_help_panel=Preset, help='Solar radiation database with hourly time resolution')]
'PVGIS-SARAH2'
consider_shadows Annotated[bool, Argument(rich_help_panel=Preset, help='Calculate effect of horizon shadowing')]
True
horizon_heights Annotated[List[float], typer_argument_horizon_heights]
None
pv_techonology Annotated[str | None, typer_argument_pv_technology]
None
mounting_type Annotated[str | None, typer_argument_mounting_type]
'free'
inclination
required
orientation
required
optimise_inclination
required
optimise_angles
required
single_axis_system Annotated[bool, Argument(rich_help_panel=Optional, help='Consider a single axis PV system -- Remove Me and improve single_axis_inclination!')]
False
single_axis_inclination Annotated[float, Argument(rich_help_panel=Optional, help='Inclination for a single axis PV system', min=0, max=90)]
0
optimise_single_axis_inclination Annotated[bool, Argument(rich_help_panel=Optional, help='Optimise inclination for a single axis PV system')]
False
vertical_axis_system Annotated[bool, Argument(rich_help_panel=Optional, help='Consider a single vertical axis PV system -- Remove Me and improve vertical_axis_inclination!')]
False
vertical_axis_inclination Annotated[float, Argument(rich_help_panel=Optional, help='Inclination for a single axis PV system', min=0, max=90)]
0
optimise_vertical_axis_inclination Annotated[bool, Argument(rich_help_panel=Optional, help='Optimise inclination for a single vertical axis PV system')]
False
two_axis_system Annotated[bool, Argument(rich_help_panel=Optional, help='Consider a two-axis tracking PV system -- Review Me!')]
False
electricity_price Annotated[bool, Argument(rich_help_panel=Optional, help='Calculate the PV electricity price (kwh/year) for the system cost in the user requested currency -- Review Me!')]
False
cost Annotated[float, Argument(rich_help_panel=Optional, help='Total cost of installing the PV system [custom currency]', min=0, max=100000000)]
0
interest Annotated[float, Argument(rich_help_panel=Optional, help='Interest in %/year', min=0, max=100)]
None
lifetime Annotated[int, Argument(rich_help_panel=Optional, help='Expected lifetime of the PV system in years', min=0, max=100)]
25
output_format Annotated[str | None, Argument(help='Output format')]
'csv'

Other Parameters:

Name Type Description
only_seldom_used_keyword int

Infrequently used parameters can be described under this optional section to prevent cluttering the Parameters section.

**kwargs dict

Other infrequently used keyword arguments. Note that all keyword arguments appearing after the first parameter specified under the Other Parameters section, should also be described under this section.

Returns:

Type Description
str

PV calculation result based on the specified input parameters

Raises:

Type Description
BadException

Because you shouldn't have done that.

See Also

estimate_energy.offgrid : Relationship (optional).

Notes

The following notes are sourced from:

  • the manual of PVcalc
  • the web GUI
  • the original C/C++ program rsun_standalone_hourly_opt

Original Parameters:

  • lat (float): Latitude in decimal degrees, south is negative. (Required)
  • lon (float): Longitude in decimal degrees, west is negative. (Required)

    • Input latitude and longitude

    Latitude and longitude can be input in the format DD:MM:SSA where DD is the degrees, MM the arc-minutes, SS the arc-seconds and A the hemisphere (N, S, E, W).

    Latitude and longitude can also be input as decimal values, so for instance 45°15'N should be input as 45.25. Latitudes south of the equator are input as negative values, north are positive. Longitudes west of the 0° meridian should be given as negative values, eastern values are positive.

  • peakpower (float): Nominal power of the PV system in kW. (Required)

    • Installed peak PV power [kWp] - Peak power

    This is the power that the manufacturer declares that the PV array can produce under standard test conditions, which are a constant 1000W of solar irradiance per square meter in the plane of the array, at an array temperature of 25°C. The peak power should be entered in kilowatt-peak (kWp). If you do not know the declared peak power of your modules but instead know the area of the modules (in m2) and the declared conversion efficiency (in percent), you can calculate the peak power as power (kWp) = 1 kW/m2 * area * efficiency / 100. See more explanation in the FAQ

  • loss (float): Sum of system losses in percent. (Required)

    • Estimated system losses

    The estimated system losses are all the losses in the system, which cause the power actually delivered to the electricity grid to be lower than the power produced by the PV modules. There are several causes for this loss, such as losses in cables, power inverters, dirt (sometimes snow) on the modules and so on. Over the years the modules also tend to lose a bit of their power, so the average yearly output over the lifetime of the system will be a few percent lower than the output in the first years.

    We have given a default value of 14% for the overall losses. If you have a good idea that your value will be different (maybe due to a really high-efficiency inverter) you may reduce this value a little.

  • raddatabase (str): Radiation database. (Default: "PVGIS-SARAH2")

    • Solar radiation databases

    • PVGIS offers four different solar radiation databases with hourly time resolution. At the moment, there are three satellite-based databases:

    • PVGIS-SARAH2 (0.05º x 0.05º) Database produced by CM SAF to replace SARAH-1 (PVGIS-SARAH). It covers Europe, Africa, most of Asia, and parts of South America. Temporal range: 2005-2020.

    • PVGIS-SARAH* (0.05º x 0.05º) Database produced using the CM SAF algorithm. Similar coverage to SARAH-2. Temporal range: 2005-2016.

    • PVGIS-NSRDB (0.04º x 0.04º) Result of a collaboration with NREL (USA) under which the NSRDB solar radiation database was made available for PVGIS. Temporal range: 2005-2015.

    In addition to these, there is also a reanalysis database available worldwide.

    • PVGIS-ERA5 (0.25º x 0.25º) Latest global reanalysis of the ECMWF (ECMWF). Temporal range: 2005-2020.

    Reanalyses solar radiation data generally have larger uncertainty than satellite-based databases. Therefore, we recommend using reanalysis data only where satellite-based data are missing or outdated. For more information about the databases and the accuracy, see the PVGIS web page on the calculation methods.

  • usehorizon (int): Consider shadows from high horizon (1) or not (0). (Default: 1)

    • Calculated horizon

    The solar radiation and PV output will change if there are local hills or mountains that block the light of the sun during some periods of the day. PVGIS can calculate the effect of this using data about ground elevation with a resolution of 3 arc-seconds (around 90m). This calculation does not take into account shadows from very nearby objects such as houses or trees. In this case you can upload your own horizon information.

    It is normally a good idea to use the horizon shadowing option.

  • userhorizon (list): Height of the horizon at equidistant directions around the point of interest, in degrees. Starting at north and moving clockwise. (Optional)

    • User-defined horizon

    PVGIS includes a database of the horizon height around each point you can choose in the region. In this way, the calculation of PV performance can take into account the effects of mountains and hills casting shadows onto the PV system. The resolution of the horizon information is 3 arc-seconds (around 90m), so objects that are very near, such as houses or trees are not included. However, you have the possibility to upload your own information about the horizon height.

    The horizon file to be uploaded to our web site should be a simple text file, such as you can create using a text editor (such as Notepad for Windows), or by exporting a spreadsheet as comma-separated values (.csv). The file name must have the extensions '.txt' or '.csv'.

    In the file there should be one number per line, with each number representing the horizon height in degrees in a certain compass direction around the point of interest. The horizon height cannot be higher than 90 degrees, so if the file contains a value higher than that, it will be automatically replaced by 90.

    The horizon heights in the file should be given in a clockwise direction starting at North; that is, from North, going to East, South, West, and back to North. The values are assumed to represent equal angular distance around the horizon. For instance, if you have 36 values in the file, PVGIS assumes that the first point is due north, the next is 10 degrees east of north, and so on, until the last point, 10 degrees west of north.

    An example file can be found here. In this case, there are only 12 numbers in the file, corresponding to a horizon height for every 30 degrees around the horizon.

  • pvtechchoice (str): PV technology choice. (Optional)

    • PV technology

    The performance of PV modules depends on the temperature and on the solar irradiance, as well as on the spectrum of the sunlight, but the exact dependence varies between different types of PV modules. At the moment we can estimate the losses due to temperature and irradiance effects for the following types of modules:

    1. crystalline silicon cells
    - thin film modules made from:
      2. CIS or CIGS
      3. Cadmium Telluride (CdTe)
    

    For other technologies (especially various amorphous technologies), this correction cannot be calculated here. If you choose one of the first three options here the calculation of performance will take into account the temperature dependence of the performance of the chosen technology. If you choose the other option (other/unknown), the calculation will assume a loss of 8% of power due to temperature effects (a generic value which was found to be reasonable for temperate climates). Note that the calculation of the effect of spectral variations is at the moment only available for crystalline silicon and for CdTe. The spectral effect cannot be considered yet for the areas only covered by the PVGIS-NSRDB database.

  • mountingplace (str): Type of mounting of the PV modules. Choices are: "free" or "building". (Default: "free")

  • fixed (bool): Calculate a fixed mounted system (1) or not (0). (Default: 1)

    • Mounting position

    For fixed (non-tracking) systems, the way the modules are mounted will have an influence on the temperature of the module, which in turn affects the efficiency. Experiments have shown that if the movement of air behind the modules is restricted, the modules can get considerably hotter (up to 15°C at 1000W/m2 of sunlight).

    In the application there are two possibilities: free-standing, meaning that the modules are mounted on a rack with air flowing freely behind the modules; and roof added / building-integrated, which means that the modules are completely built into the structure of the wall or roof of a building, with little or no air movement behind the modules.

    Some types of mounting are in between these two extremes, for instance if the modules are mounted on a roof with curved roof tiles, allowing air to move behind the modules. In such cases, the performance will be somewhere between the results of the two calculations that are possible here. For such cases, to be conservative, the roof added / building integrated option can be used.

  • angle (float): Inclination angle from horizontal plane of the fixed PV system. (Optional)

    • Inclination angle or slope

    This is the angle of the PV modules from the horizontal plane, for a fixed (non-tracking) mounting.

    For some applications the slope and orientation angles will already be known, for instance if the PV modules are to be built into an existing roof. However, if you have the possibility to choose the slope and/or azimuth (orientation), this application can also calculate for you the optimal values for slope and orientation (assuming fixed angles for the entire year).

  • aspect (float): Orientation (azimuth) angle of the fixed PV system. (Optional)

    • Orientation angle or azimuth

    The azimuth, or orientation, is the angle of the PV modules relative to the direction due South. -90° is East, 0° is South and 90° is West.

    For some applications the slope and azimuth angles will already be known, for instance if the PV modules are to be built into an existing roof. However, if you have the possibility to choose the inclination and/or orientation, this application can also calculate for you the optimal values for inclination and orientation (assuming fixed angles for the entire year).

  • optimalinclination (bool): Calculate the optimum inclination angle (1) or not (0). (Optional)

    • Optimize slope

    If you click to choose this option, PVGIS will calculate the slope of the PV modules that gives the highest energy output for the whole year. This assumes that the slope angle stays fixed for the entire year.

  • optimalangles (bool): Calculate the optimum inclination and orientation angles (1) or not (0). (Optional)

    • Optimize inclination and azimuth

    If you click to choose this option, PVGIS will calculate the inclination AND orientation/azimuth of the PV modules that gives the highest energy output for the whole year. This assumes that the mounting of the PV modules stays fixed for the entire year.

  • inclined_axis (bool): Calculate a single inclined axis system (1) or not (0). (Optional)

    • Inclined Axis

    In this type of PV system, the modules are mounted on a structure rotating around an axis that forms an angle with the ground and points in the north-south direction. The plane of the modules is assumed to be parallel to the axis of rotation. It is assumed that the axis rotates during the day such that the angle to the sun is always as small as possible (this means that it will not rotate at constant speed during the day). The angle of the axis relative to the ground can be given, or you can ask to calculate the optimal angle for your location.

  • inclinedaxisangle (float): Inclination angle for a single inclined axis system. (Optional)

    • Inclination angle or slope

    This is the angle of the PV modules from the horizontal plane, for a fixed (non-tracking) mounting.

    For some applications the slope and orientation angles will already be known, for instance if the PV modules are to be built into an existing roof. However, if you have the possibility to choose the slope and/or azimuth (orientation), this application can also calculate for you the optimal values for slope and orientation (assuming fixed angles for the entire year).

  • inclined_optimum (bool): Calculate optimum angle for a single inclined axis system (1) or not (0). (Optional)

    • Optimize inclination of inclined-axis mounting

    If you click to choose this option, PVGIS will calculate the inclination of the inclined rotating axis that the PV modules are mounted on which gives the highest energy output for the whole year.

  • vertical_axis (bool): Calculate a single vertical axis system (1) or not (0). (Optional)

    • Vertical Axis

    In this type of PV system, the modules are mounted on a moving structure with a vertical rotating axis, at an angle. The structure rotates around the axis during the day such that the angle to the sun is always as small as possible (this means that it will not rotate at constant speed during the day). The angle of the modules relative to the ground can be given, or you can ask to calculate the optimal angle for your location.

  • verticalaxisangle (float): Inclination angle for a single vertical axis system. (Optional)

    • Inclination angle or slope

    This is the angle of the PV modules from the horizontal plane, for a fixed (non-tracking) mounting. For some applications the slope and orientation angles will already be known, for instance if the PV modules are to be built into an existing roof. However, if you have the possibility to choose the slope and/or azimuth (orientation), this application can also calculate for you the optimal values for slope and orientation (assuming fixed angles for the entire year).

  • vertical_optimum (bool): Calculate optimum angle for a single vertical axis system (1) or not (0). (Optional)

    • Optimize slope

    If you click to choose this option, PVGIS will calculate the slope of the PV modules that gives the highest energy output for the whole year. This assumes that the slope angle stays fixed for the entire year.

  • twoaxis (bool): Calculate a two-axis tracking system (1) or not (0). (Optional)

    • Two Axis

    In this type of PV system, the modules are mounted on a structure that can move the modules in the east-west direction and also tilt them at an angle from the ground, so that the modules always point at the sun. Note that the calculation still assumes that the modules do not concentrate the light directly from the sun, but can use all the light falling on the modules, both that coming directly from the sun and that coming from the rest of the sky.

  • pvprice (bool): Calculate the PV electricity price (1) or not (0). (Optional)

  • systemcost (float): Total cost of installing the PV system in your currency. (Required if pvprice=1)

    • PV system cost

    Here you should input the total cost of installing the PV system, including PV system components (PV modules, mounting, inverters, cables, etc.) and installation costs (planning, installation, ...). The choice of currency is up to you, the electricity price calculated by PVGIS will then be the price per kWh of electricity in the same currency you used.

  • interest (float): Interest in %/year. (Required if pvprice=1)

    • Interest rate

    This is the interest rate you pay on any loans needed to finance the PV system. This assumes a fixed interest rate on the loan which will be paid back in yearly instalments over the lifetime of the system.

  • lifetime (int): Expected lifetime of the PV system in years. (Required if pvprice=1)

    • PV system lifetime

    This is the expected lifetime of the PV system in years. This is used to calculate the effective electricity cost for the system. If the PV system happens to last longer the electricity cost will be correspondingly lower.

  • outputformat (str): Output format. Choices: "csv", "basic", "json". (Default: "csv")

  • browser (bool): Setting browser=1 and accessing the service through a web browser will save the retrieved data to a file. (Default: 0)

Additional notes:

  • For the fixed PV system, if the parameter optimalinclination is set to 1, the value defined for the angle parameter is ignored. Similarly, if optimalangles is set to 1, values defined for angle and aspect are ignored. In which case the parameter ptimalinclination is not required either.

  • For the inclined axis PV system analysis, the parameter inclined_axis must be selected along with either inclinedaxisangle or inclined_optimum.

  • If the parameter inclined_optimum is selected, the inclination angle defined in inclinedaxisangle is ignored, so this parameter would not be necessary.

  • Parameters regarding the vertical axis (vertical_axis, vertical_optimum and verticalaxisangle) are related in the same way as the parameters used for the inclined axis PV system.

References

Cite the relevant literature, e.g. [1]_. You may also cite these references in the notes section above.

.. [1]

Examples:

Source code in pvgisprototype/cli/power/energy_.py
@app.command(
    "grid",
    no_args_is_help=True,
    help=f":electric_plug: Estimate the energy production of a PV system connected to the electricity grid {NOT_IMPLEMENTED}",
)
def estimate_grid_connected_pv(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    peak_power: Annotated[
        float,
        typer.Argument(
            rich_help_panel="Required",
            help="The installed peak PV power in kWp",
            min=0,
            max=100000000,
        ),
    ],  # peakpower
    loss: Annotated[
        float,
        typer.Argument(
            rich_help_panel="Preset", help="System losses in %", min=0, max=100
        ),
    ] = 14,  # loss
    solar_radiation_database: Annotated[
        str | None,
        typer.Argument(
            rich_help_panel="Preset",
            help="Solar radiation database with hourly time resolution",
        ),
    ] = "PVGIS-SARAH2",  # raddatabase
    consider_shadows: Annotated[
        bool,
        typer.Argument(
            rich_help_panel="Preset", help="Calculate effect of horizon shadowing"
        ),
    ] = True,  # usehorizon
    horizon_heights: Annotated[List[float], typer_argument_horizon_heights] = None,
    pv_techonology: Annotated[
        str | None, typer_argument_pv_technology
    ] = None,  # pvtechchoice
    mounting_type: Annotated[str | None, typer_argument_mounting_type] = "free",
    surface_orientation: Annotated[
        float, typer_argument_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[float, typer_argument_surface_tilt] = SURFACE_TILT_DEFAULT,
    optimise_surface_tilt: Annotated[
        bool, typer_option_optimise_surface_tilt
    ] = OPTIMISE_SURFACE_TILT_FLAG_DEFAULT,
    optimise_surface_geometry: Annotated[
        bool, typer_option_optimise_surface_geometry
    ] = OPTIMISE_SURFACE_GEOMETRY_FLAG_DEFAULT,
    single_axis_system: Annotated[
        bool,
        typer.Argument(
            rich_help_panel="Optional",
            help="Consider a single axis PV system -- Remove Me and improve single_axis_inclination!",
        ),
    ] = False,  # inclined_axis
    single_axis_inclination: Annotated[
        float,
        typer.Argument(
            rich_help_panel="Optional",
            help="Inclination for a single axis PV system",
            min=0,
            max=90,
        ),
    ] = 0,  # inclinedaxisangle
    optimise_single_axis_inclination: Annotated[
        bool,
        typer.Argument(
            rich_help_panel="Optional",
            help="Optimise inclination for a single axis PV system",
        ),
    ] = False,  # inclined_optimum
    vertical_axis_system: Annotated[
        bool,
        typer.Argument(
            rich_help_panel="Optional",
            help="Consider a single vertical axis PV system -- Remove Me and improve vertical_axis_inclination!",
        ),
    ] = False,  # vertical_axis
    vertical_axis_inclination: Annotated[
        float,
        typer.Argument(
            rich_help_panel="Optional",
            help="Inclination for a single axis PV system",
            min=0,
            max=90,
        ),
    ] = 0,  # Verticalaxisangle
    optimise_vertical_axis_inclination: Annotated[
        bool,
        typer.Argument(
            rich_help_panel="Optional",
            help="Optimise inclination for a single vertical axis PV system",
        ),
    ] = False,  # vertical_optimum
    two_axis_system: Annotated[
        bool,
        typer.Argument(
            rich_help_panel="Optional",
            help="Consider a two-axis tracking PV system -- Review Me!",
        ),
    ] = False,  # twoaxis
    electricity_price: Annotated[
        bool,
        typer.Argument(
            rich_help_panel="Optional",
            help="Calculate the PV electricity price (kwh/year) for the system cost in the user requested currency -- Review Me!",
        ),
    ] = False,  # pvprice
    cost: Annotated[
        float,
        typer.Argument(
            rich_help_panel="Optional",
            help="Total cost of installing the PV system [custom currency]",
            min=0,
            max=100000000,
        ),
    ] = 0,  # systemcost
    interest: Annotated[
        float,
        typer.Argument(
            rich_help_panel="Optional", help="Interest in %/year", min=0, max=100
        ),
    ] = None,  # interest
    lifetime: Annotated[
        int,
        typer.Argument(
            rich_help_panel="Optional",
            help="Expected lifetime of the PV system in years",
            min=0,
            max=100,
        ),
    ] = 25,  # lifetime
    output_format: Annotated[
        str | None, typer.Argument(help="Output format")
    ] = "csv",  # outputformat
):
    r"""Estimate the energy production of a PV system connected to the electricity grid

    Performance of grid-connected PV systems.  This tool makes it possible to
    estimate the average monthly and yearly energy production of a PV system
    connected to the electricity grid, without battery storage. The calculation
    takes into account the solar radiation, temperature, wind speed and type of
    PV module. The user can choose how the modules are mounted, whether on a
    free-standing rack mounting, sun-tracking mountings or integrated in a
    building surface. PVGIS can also calculate the optimum slope and
    orientation that maximizes the yearly energy production.

    Requires following data:

        - elevation
        - horizon
        - dem_era5
        - tgrad_bin
        - sis
        - sid
        - temperature (ERA5 t2m)
        - wind speed (ERA5 w2m)
        - pv coefficients (current file: pvtech.coeffs)
        - pv coefficients for bifacial? (current file: pvtech.coeffs_bipv)
        - spectral correction data (current file: pvtech_spectraldata.bin)

    Parameters
    ----------
    longitude: float
    latitude: float
    peak_power: float
    loss: float
    solar_radiation_database: {'PVGIS-SARAH2', 'PVGIS-SARAH', 'PVGIS-NSRDB', 'PVGIS-ERA5'}, optional
    consider_shadows: bool
    horizon_heights: int
    pv_techonology: str
    mounting_type: str
    inclination: float
    orientation: float
    optimise_inclination: bool
    optimise_angles: bool
    single_axis_system: bool
    single_axis_inclination: float
    optimise_single_axis_inclination: bool
    vertical_axis_system: bool
    vertical_axis_inclination: float
    optimise_vertical_axis_inclination: bool
    two_axis_system: bool
    electricity_price: bool
    cost: float
    interest: float
    lifetime: int
    output_format: str

    Other Parameters
    ----------------
    only_seldom_used_keyword : int, optional
        Infrequently used parameters can be described under this optional
        section to prevent cluttering the Parameters section.
    **kwargs : dict
        Other infrequently used keyword arguments. Note that all keyword
        arguments appearing after the first parameter specified under the
        Other Parameters section, should also be described under this
        section.

    Returns
    -------
    str
        PV calculation result based on the specified input parameters

    Raises
    ------
    BadException
        Because you shouldn't have done that.

    See Also
    --------
    estimate_energy.offgrid : Relationship (optional).

    Notes
    -----

    The following notes are sourced from:

    - the manual of PVcalc
    - the web GUI
    - the original C/C++ program `rsun_standalone_hourly_opt`

    Original Parameters:

    - lat (float): Latitude in decimal degrees, south is negative. (Required)
    - lon (float): Longitude in decimal degrees, west is negative. (Required)

        - Input latitude and longitude

        Latitude and longitude can be input in the format DD:MM:SSA where
        DD is the degrees, MM the arc-minutes, SS the arc-seconds and A the
        hemisphere (N, S, E, W).

        Latitude and longitude can also be input as decimal values, so for
        instance 45°15'N should be input as 45.25. Latitudes south of the
        equator are input as negative values, north are positive.
        Longitudes west of the 0° meridian should be given as negative
        values, eastern values are positive.

    - peakpower (float): Nominal power of the PV system in kW. (Required)

        - Installed peak PV power [kWp] - Peak power

        This is the power that the manufacturer declares that the PV array
        can produce under standard test conditions, which are a constant
        1000W of solar irradiance per square meter in the plane of the
        array, at an array temperature of 25°C. The peak power should be
        entered in kilowatt-peak (kWp). If you do not know the declared
        peak power of your modules but instead know the area of the modules
        (in m2) and the declared conversion efficiency (in percent), you
        can calculate the peak power as power (kWp) = 1 kW/m2 * area *
        efficiency / 100. See more explanation in the FAQ

    - loss (float): Sum of system losses in percent. (Required)

        - Estimated system losses

        The estimated system losses are all the losses in the system, which
        cause the power actually delivered to the electricity grid to be
        lower than the power produced by the PV modules. There are several
        causes for this loss, such as losses in cables, power inverters,
        dirt (sometimes snow) on the modules and so on. Over the years the
        modules also tend to lose a bit of their power, so the average
        yearly output over the lifetime of the system will be a few percent
        lower than the output in the first years.

        We have given a default value of 14% for the overall losses. If you
        have a good idea that your value will be different (maybe due to a
        really high-efficiency inverter) you may reduce this value a
        little.

    - raddatabase (str): Radiation database. (Default: "PVGIS-SARAH2")

        - Solar radiation databases

        - PVGIS offers four different solar radiation databases with hourly
          time resolution. At the moment, there are three satellite-based
          databases:

          - PVGIS-SARAH2 (0.05º x 0.05º) Database produced by CM SAF to
            replace SARAH-1 (PVGIS-SARAH). It covers Europe, Africa, most
            of Asia, and parts of South America. Temporal range: 2005-2020.

          - PVGIS-SARAH* (0.05º x 0.05º) Database produced using the CM SAF
            algorithm. Similar coverage to SARAH-2. Temporal range:
            2005-2016.

          - PVGIS-NSRDB (0.04º x 0.04º) Result of a collaboration with NREL
            (USA) under which the NSRDB solar radiation database was made
            available for PVGIS. Temporal range: 2005-2015.

        In addition to these, there is also a reanalysis database available
        worldwide.

          - PVGIS-ERA5 (0.25º x 0.25º) Latest global reanalysis of the
            ECMWF (ECMWF). Temporal range: 2005-2020.

        Reanalyses solar radiation data generally have larger uncertainty
        than satellite-based databases. Therefore, we recommend using
        reanalysis data only where satellite-based data are missing or
        outdated. For more information about the databases and the
        accuracy, see the PVGIS web page on the calculation methods.


    - usehorizon (int): Consider shadows from high horizon (1) or not (0). (Default: 1)

        - Calculated horizon

        The solar radiation and PV output will change if there are local
        hills or mountains that block the light of the sun during some
        periods of the day. PVGIS can calculate the effect of this using
        data about ground elevation with a resolution of 3 arc-seconds
        (around 90m). This calculation does not take into account shadows
        from very nearby objects such as houses or trees. In this case you
        can upload your own horizon information.

        It is normally a good idea to use the horizon shadowing option.

    - userhorizon (list): Height of the horizon at equidistant directions
      around the point of interest, in degrees. Starting at north and
      moving clockwise. (Optional)

        - User-defined horizon

        PVGIS includes a database of the horizon height around each point
        you can choose in the region. In this way, the calculation of PV
        performance can take into account the effects of mountains and
        hills casting shadows onto the PV system. The resolution of the
        horizon information is 3 arc-seconds (around 90m), so objects that
        are very near, such as houses or trees are not included. However,
        you have the possibility to upload your own information about the
        horizon height.

        The horizon file to be uploaded to our web site should be a simple
        text file, such as you can create using a text editor (such as
        Notepad for Windows), or by exporting a spreadsheet as
        comma-separated values (.csv). The file name must have the
        extensions '.txt' or '.csv'.

        In the file there should be one number per line, with each number
        representing the horizon height in degrees in a certain compass
        direction around the point of interest. The horizon height cannot
        be higher than 90 degrees, so if the file contains a value higher
        than that, it will be automatically replaced by 90.

        The horizon heights in the file should be given in a clockwise
        direction starting at North; that is, from North, going to East,
        South, West, and back to North. The values are assumed to represent
        equal angular distance around the horizon. For instance, if you
        have 36 values in the file, PVGIS assumes that the first point is
        due north, the next is 10 degrees east of north, and so on, until
        the last point, 10 degrees west of north.

        An example file can be found here. In this case, there are only 12
        numbers in the file, corresponding to a horizon height for every 30
        degrees around the horizon.

    - pvtechchoice (str): PV technology choice. (Optional)

        - PV technology

        The performance of PV modules depends on the temperature and on the
        solar irradiance, as well as on the spectrum of the sunlight, but
        the exact dependence varies between different types of PV modules.
        At the moment we can estimate the losses due to temperature and
        irradiance effects for the following types of modules:

            1. crystalline silicon cells
            - thin film modules made from:
              2. CIS or CIGS
              3. Cadmium Telluride (CdTe)

        For other technologies (especially various amorphous technologies),
        this correction cannot be calculated here. If you choose one of the
        first three options here the calculation of performance will take
        into account the temperature dependence of the performance of the
        chosen technology. If you choose the other option (other/unknown),
        the calculation will assume a loss of 8% of power due to
        temperature effects (a generic value which was found to be
        reasonable for temperate climates). Note that the calculation of
        the effect of spectral variations is at the moment only available
        for crystalline silicon and for CdTe. The spectral effect cannot be
        considered yet for the areas only covered by the PVGIS-NSRDB
        database.


    - mountingplace (str): Type of mounting of the PV modules. Choices are: "free" or "building". (Default: "free")

    - fixed (bool): Calculate a fixed mounted system (1) or not (0). (Default: 1)

        - Mounting position

        For fixed (non-tracking) systems, the way the modules are mounted
        will have an influence on the temperature of the module, which in
        turn affects the efficiency. Experiments have shown that if the
        movement of air behind the modules is restricted, the modules can
        get considerably hotter (up to 15°C at 1000W/m2 of sunlight).

        In the application there are two possibilities: free-standing,
        meaning that the modules are mounted on a rack with air flowing
        freely behind the modules; and roof added / building-integrated,
        which means that the modules are completely built into the
        structure of the wall or roof of a building, with little or no air
        movement behind the modules.

        Some types of mounting are in between these two extremes, for
        instance if the modules are mounted on a roof with curved roof
        tiles, allowing air to move behind the modules. In such cases, the
        performance will be somewhere between the results of the two
        calculations that are possible here. For such cases, to be
        conservative, the roof added / building integrated option can be
        used.

    - angle (float): Inclination angle from horizontal plane of the fixed PV system. (Optional)

        - Inclination angle or slope

        This is the angle of the PV modules from the horizontal plane, for
        a fixed (non-tracking) mounting.

        For some applications the slope and orientation angles will already
        be known, for instance if the PV modules are to be built into an
        existing roof. However, if you have the possibility to choose the
        slope and/or azimuth (orientation), this application can also
        calculate for you the optimal values for slope and orientation
        (assuming fixed angles for the entire year).

    - aspect (float): Orientation (azimuth) angle of the fixed PV system. (Optional)

        - Orientation angle or azimuth

        The azimuth, or orientation, is the angle of the PV modules
        relative to the direction due South. -90° is East, 0° is South and
        90° is West.

        For some applications the slope and azimuth angles will already be
        known, for instance if the PV modules are to be built into an
        existing roof. However, if you have the possibility to choose the
        inclination and/or orientation, this application can also calculate
        for you the optimal values for inclination and orientation
        (assuming fixed angles for the entire year).

    - optimalinclination (bool): Calculate the optimum inclination angle (1) or not (0). (Optional)

        - Optimize slope

        If you click to choose this option, PVGIS will calculate the slope
        of the PV modules that gives the highest energy output for the
        whole year. This assumes that the slope angle stays fixed for the
        entire year.

    - optimalangles (bool): Calculate the optimum inclination and orientation angles (1) or not (0). (Optional)

        - Optimize inclination and azimuth

        If you click to choose this option, PVGIS will calculate the
        inclination AND orientation/azimuth of the PV modules that gives
        the highest energy output for the whole year. This assumes that the
        mounting of the PV modules stays fixed for the entire year.

    - inclined_axis (bool): Calculate a single inclined axis system (1) or not (0). (Optional)

        - Inclined Axis

        In this type of PV system, the modules are mounted on a structure
        rotating around an axis that forms an angle with the ground and
        points in the north-south direction. The plane of the modules is
        assumed to be parallel to the axis of rotation. It is assumed that
        the axis rotates during the day such that the angle to the sun is
        always as small as possible (this means that it will not rotate at
        constant speed during the day). The angle of the axis relative to
        the ground can be given, or you can ask to calculate the optimal
        angle for your location.

    - inclinedaxisangle (float): Inclination angle for a single inclined axis system. (Optional)

        - Inclination angle or slope

        This is the angle of the PV modules from the horizontal plane, for
        a fixed (non-tracking) mounting.

        For some applications the slope and orientation angles will already
        be known, for instance if the PV modules are to be built into an
        existing roof. However, if you have the possibility to choose the
        slope and/or azimuth (orientation), this application can also
        calculate for you the optimal values for slope and orientation
        (assuming fixed angles for the entire year).

    - inclined_optimum (bool): Calculate optimum angle for a single inclined axis system (1) or not (0). (Optional)

        - Optimize inclination of inclined-axis mounting

        If you click to choose this option, PVGIS will calculate the
        inclination of the inclined rotating axis that the PV modules are
        mounted on which gives the highest energy output for the whole
        year.

    - vertical_axis (bool): Calculate a single vertical axis system (1) or not (0). (Optional)

        - Vertical Axis

        In this type of PV system, the modules are mounted on a moving
        structure with a vertical rotating axis, at an angle. The structure
        rotates around the axis during the day such that the angle to the
        sun is always as small as possible (this means that it will not
        rotate at constant speed during the day). The angle of the modules
        relative to the ground can be given, or you can ask to calculate
        the optimal angle for your location.

    - verticalaxisangle (float): Inclination angle for a single vertical axis system. (Optional)

        - Inclination angle or slope

        This is the angle of the PV modules from the horizontal plane, for
        a fixed (non-tracking) mounting. For some applications the slope
        and orientation angles will already be known, for instance if the
        PV modules are to be built into an existing roof. However, if you
        have the possibility to choose the slope and/or azimuth
        (orientation), this application can also calculate for you the
        optimal values for slope and orientation (assuming fixed angles for
        the entire year).

    - vertical_optimum (bool): Calculate optimum angle for a single vertical axis system (1) or not (0). (Optional)

        - Optimize slope

        If you click to choose this option, PVGIS will calculate the slope
        of the PV modules that gives the highest energy output for the
        whole year. This assumes that the slope angle stays fixed for the
        entire year.

    - twoaxis (bool): Calculate a two-axis tracking system (1) or not (0). (Optional)

        - Two Axis

        In this type of PV system, the modules are mounted on a structure
        that can move the modules in the east-west direction and also tilt
        them at an angle from the ground, so that the modules always point
        at the sun. Note that the calculation still assumes that the
        modules do not concentrate the light directly from the sun, but can
        use all the light falling on the modules, both that coming directly
        from the sun and that coming from the rest of the sky.

    - pvprice (bool): Calculate the PV electricity price (1) or not (0). (Optional)
    - systemcost (float): Total cost of installing the PV system in your currency. (Required if pvprice=1)
        - PV system cost

        Here you should input the total cost of installing the PV system,
        including PV system components (PV modules, mounting, inverters,
        cables, etc.) and installation costs (planning, installation, ...).
        The choice of currency is up to you, the electricity price
        calculated by PVGIS will then be the price per kWh of electricity
        in the same currency you used.

    - interest (float): Interest in %/year. (Required if pvprice=1)

        - Interest rate

        This is the interest rate you pay on any loans needed to finance
        the PV system. This assumes a fixed interest rate on the loan which
        will be paid back in yearly instalments over the lifetime of the
        system.

    - lifetime (int): Expected lifetime of the PV system in years. (Required if pvprice=1)

        - PV system lifetime

        This is the expected lifetime of the PV system in years. This is
        used to calculate the effective electricity cost for the system. If
        the PV system happens to last longer the electricity cost will be
        correspondingly lower.

    - outputformat (str): Output format. Choices: "csv", "basic", "json". (Default: "csv")
    - browser (bool): Setting browser=1 and accessing the service through a
      web browser will save the retrieved data to a file. (Default: 0)

    Additional notes:

    - For the fixed PV system, if the parameter `optimalinclination` is set to `1`,
      the value defined for the `angle` parameter is ignored.
      Similarly, if `optimalangles` is set to 1, values defined for `angle` and `aspect`
      are ignored. In which case the parameter `ptimalinclination` is not required either.

    - For the inclined axis PV system analysis,
      the parameter `inclined_axis` must be selected along with
      either `inclinedaxisangle` or `inclined_optimum`.

    - If the parameter `inclined_optimum` is selected,
      the inclination angle defined in `inclinedaxisangle` is ignored,
      so this parameter would not be necessary.

    - Parameters regarding the vertical axis
      (`vertical_axis`, `vertical_optimum` and `verticalaxisangle`)
      are related in the same way as the parameters used for the inclined axis PV system.

    References
    ----------
    Cite the relevant literature, e.g. [1]_.  You may also cite these
    references in the notes section above.

    .. [1]

    Examples
    --------

    """
    # Fake an output for now! -------------------------------------------------
    path_to_module = os.path.dirname(__file__)
    path_to_test_data = pathlib.Path(path_to_module).parent / "tests" / "data"
    csv_files = path_to_test_data.glob("*.csv")
    for csv_file in csv_files:
        output_csv = csv_to_list_of_dictionaries(csv_file)
        print(output_csv)
    # ------------------------------------------------# Fake an output for now!
    return 0

estimate_offgrid_pv

estimate_offgrid_pv()

Estimate the energy production of a PV system that is not connected to the electricity grid but instead relies on battery storage

Performance of off-grid PV systems. This part of PVGIS calculates the performance of PV systems that are not connected to the electricity grid but instead rely on battery storage to supply energy when the sun is not shining. The calculation uses information about the daily variation in electricity consumption for the system to simulate the flow of energy to the users and into and out of the battery.

Source code in pvgisprototype/cli/power/energy_.py
@app.command(
    "offgrid",
    no_args_is_help=True,
    help=f":battery:  Estimate PV power output for a system not connected to the grid {NOT_IMPLEMENTED}",
)
def estimate_offgrid_pv():
    """Estimate the energy production of a PV system that is not connected to
    the electricity grid but instead relies on battery storage

    Performance of off-grid PV systems. This part of PVGIS calculates the
    performance of PV systems that are not connected to the electricity grid
    but instead rely on battery storage to supply energy when the sun is not
    shining. The calculation uses information about the daily variation in
    electricity consumption for the system to simulate the flow of energy to
    the users and into and out of the battery.
    """
    print(f"{NOT_IMPLEMENTED}")

estimate_tracking_pv

estimate_tracking_pv()

Estimate the energy production of a tracking PV system connected to the electricity grid

Performance of tracking PV. PV modules can be placed on mountings that move the PV modules to allow them to follow (track) the movement of the sun in the sky. In this way we can increase the amount of sunlight arriving at the PV modules. This movement can be made in several different ways. Here we give three options:

  • Vertical axis: The modules are mounted on a moving structure with a vertical rotating axis, at an angle. The structure rotates around the axis during the day such that the angle to the sun is always as small as possible (this means that it will not rotate at constant speed during the day). The angle of the modules relative to the ground can be given, or you can ask to calculate the optimal angle for your location.

  • Inclined axis: The modules are mounted on an structure rotating around an axis that forms an angle with the ground and points in the north-south direction. The plane of the modules is assumed to be parallel to the axis of rotation. It is assumed that the axis rotates during the day such that the angle to the sun is always as small as possible (this means that it will not rotate at constant speed during the day). The angle of the axis relative to the ground can be given, or you can ask to calculate the optimal angle for your location.

  • Two-axis tracker: The modules are mounted on a system that can move the modules in the east-west direction and also tilt them at an angle from the ground, so that the modules always point at the sun. Note that the calculation still assumes that the modules do not concentrate the light directly from the sun, but can use all the light falling on the modules, both that coming directly from the sun and that coming from the rest of the sky.

Source code in pvgisprototype/cli/power/energy_.py
@app.command(
    "tracking",
    no_args_is_help=True,
    help=f":satellite_antenna: Estimate PV power output for a system not connected to the grid {NOT_IMPLEMENTED}",
)
def estimate_tracking_pv():
    """Estimate the energy production of a tracking PV system connected to the electricity grid

    Performance of tracking PV.  PV modules can be placed on mountings that
    move the PV modules to allow them to follow (track) the movement of the sun
    in the sky. In this way we can increase the amount of sunlight arriving at
    the PV modules. This movement can be made in several different ways. Here
    we give three options:

    - Vertical axis: The modules are mounted on a moving structure with a
      vertical rotating axis, at an angle. The structure rotates around the
      axis during the day such that the angle to the sun is always as small as
      possible (this means that it will not rotate at constant speed during the
      day). The angle of the modules relative to the ground can be given, or
      you can ask to calculate the optimal angle for your location.

    - Inclined axis: The modules are mounted on an structure rotating around an
      axis that forms an angle with the ground and points in the north-south
      direction. The plane of the modules is assumed to be parallel to the axis
      of rotation. It is assumed that the axis rotates during the day such that
      the angle to the sun is always as small as possible (this means that it
      will not rotate at constant speed during the day). The angle of the axis
      relative to the ground can be given, or you can ask to calculate the
      optimal angle for your location.

    - Two-axis tracker: The modules are mounted on a system that can move the
      modules in the east-west direction and also tilt them at an angle from
      the ground, so that the modules always point at the sun. Note that the
      calculation still assumes that the modules do not concentrate the light
      directly from the sun, but can use all the light falling on the modules,
      both that coming directly from the sun and that coming from the rest of
      the sky.
    """
    print(f"{NOT_IMPLEMENTED}")

introduction

Functions:

Name Description
photovoltaic_power_introduction

A short introduction on photovoltaic performance

photovoltaic_power_introduction

photovoltaic_power_introduction()

A short introduction on photovoltaic performance

Source code in pvgisprototype/cli/power/introduction.py
def photovoltaic_power_introduction():
    """A short introduction on photovoltaic performance"""
    introduction = """
    [underline]Photovoltaic power[/underline] is the [blue]electrical
    power[/blue] generated by a photovoltaic (PV) system when solar irradiance
    strikes its surface and is converted into electricity.

    This process relies on the [italic]photovoltaic effect[/italic], where
    solar cells convert sunlight into direct current (DC) electricity, which
    can then be used, stored, or fed into the electrical grid. The amount of
    power produced by a PV system depends on various factors such as the
    [italic]angle of sunlight[/italic], the [italic]efficiency[/italic] of the
    solar panels, environmental conditions, and the available solar irradiance.

    """

    note = """
    PVGIS can estimate the photovoltaic power output for a series of
    technologies using either [magenta]broadband[/magenta] or
    [magenta]spectrally resolved[/magenta] irradiance data.

    """
    from rich.panel import Panel

    note_in_a_panel = Panel(
        "[italic]{}[/italic]".format(note),
        title="[bold cyan]Note[/bold cyan]",
        width=78,
    )
    from rich.console import Console

    console = Console()
    # introduction.wrap(console, 30)
    console.print(introduction)
    console.print(note_in_a_panel)
    console.print(A_PRIMER_ON_PHOTOVOLTAIC_PERFORMANCE)

power

Functions:

Name Description
calculate_peak_power

Calculate the peak power in kW based on area and conversion efficiency

calculate_peak_power

calculate_peak_power(
    area: Annotated[float, typer_argument_area],
    conversion_efficiency: Annotated[
        float, typer_argument_conversion_efficiency
    ],
)

Calculate the peak power in kW based on area and conversion efficiency

.. math:: Power = 1/m^{2} * area * efficiency / 100

Returns: Power in kWp

Source code in pvgisprototype/cli/power/power.py
@app.command(
    "peak-power",
    no_args_is_help=True,
    help=f"{PEAK_POWER_UNIT} Calculate the peak power in kW based on area and conversion efficiency {NOT_IMPLEMENTED_CLI}",
    rich_help_panel=rich_help_panel_power_toolbox,
)
def calculate_peak_power(
    area: Annotated[float, typer_argument_area],
    conversion_efficiency: Annotated[float, typer_argument_conversion_efficiency],
):
    """Calculate the peak power in kW based on area and conversion efficiency

    .. math:: Power = 1/m^{2} * area * efficiency / 100

    Returns:
        Power in kWp
    """
    power = 1 / area * conversion_efficiency / 100
    return power

spectral

Functions:

Name Description
spectral_photovoltaic_power_output_series

This method accounts for the effects of the solar spectrum's varying

spectral_photovoltaic_power_output_series

spectral_photovoltaic_power_output_series(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None, typer_option_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_option_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        str | None, typer_option_timezone
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    spectrally_resolved_global_horizontal_irradiance_series: Annotated[
        Path | None,
        typer_option_global_horizontal_irradiance,
    ] = None,
    spectrally_resolved_direct_horizontal_irradiance_series: Annotated[
        Path | None,
        typer_option_direct_horizontal_irradiance,
    ] = None,
    number_of_junctions: int = 1,
    spectral_response_data: Path | None = None,
    standard_conditions_response: Path | None = None,
    minimum_spectral_mismatch=MINIMUM_SPECTRAL_MISMATCH,
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor,
        typer_option_linke_turbidity_factor_series,
    ] = None,
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[
        float | None, typer_option_albedo
    ] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = True,
    solar_position_model: Annotated[
        SolarPositionModel,
        typer_option_solar_position_model,
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel,
        typer_option_solar_incidence_model,
    ] = iqbal,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[
        float, typer_option_solar_constant
    ] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[
        float, typer_option_eccentricity_phase_offset
    ] = value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = value,
    time_output_units: Annotated[
        str, typer_option_time_output_units
    ] = MINUTES,
    angle_units: Annotated[
        str, typer_option_angle_units
    ] = RADIANS,
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = RADIANS,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel,
        typer_option_pv_power_algorithm,
    ] = king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm,
        typer_option_module_temperature_algorithm,
    ] = faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
)

This method accounts for the effects of the solar spectrum's varying wavelengths on PV output, offering a more detailed analysis for systems sensitive to specific spectral ranges.

Source code in pvgisprototype/cli/power/spectral.py
def spectral_photovoltaic_power_output_series(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
    elevation: Annotated[float, typer_argument_elevation],
    surface_orientation: Annotated[
        float | None, typer_option_surface_orientation
    ] = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: Annotated[
        float | None, typer_option_surface_tilt
    ] = SURFACE_TILT_DEFAULT,
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[datetime | None, typer_option_start_time] = None,
    periods: Annotated[int | None, typer_option_periods] = None,
    frequency: Annotated[str | None, typer_option_frequency] = None,
    end_time: Annotated[datetime | None, typer_option_end_time] = None,
    timezone: Annotated[str | None, typer_option_timezone] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    spectrally_resolved_global_horizontal_irradiance_series: Annotated[
        Path | None, typer_option_global_horizontal_irradiance
    ] = None,
    spectrally_resolved_direct_horizontal_irradiance_series: Annotated[
        Path | None, typer_option_direct_horizontal_irradiance
    ] = None,
    number_of_junctions: int = 1,
    spectral_response_data: Path | None = None,
    standard_conditions_response: Path | None = None,  #: float = 1,  # STCresponse : read from external data
    # extraterrestrial_normal_irradiance_series,  # spectral_ext,
    minimum_spectral_mismatch=MINIMUM_SPECTRAL_MISMATCH,
    temperature_series: Annotated[
        TemperatureSeries, typer_argument_temperature_series
    ] = TEMPERATURE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_argument_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    # dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    # array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    # multi_thread: Annotated[bool, typer_option_multi_thread] = MULTI_THREAD_FLAG_DEFAULT,
    linke_turbidity_factor_series: Annotated[
        LinkeTurbidityFactor, typer_option_linke_turbidity_factor_series
    ] = None,  # Changed this to np.ndarray
    adjust_for_atmospheric_refraction: Annotated[
        bool, typer_option_adjust_for_atmospheric_refraction
    ] = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: Annotated[
        float | None, typer_option_refracted_solar_zenith
    ] = UNREFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    albedo: Annotated[float | None, typer_option_albedo] = ALBEDO_DEFAULT,
    apply_reflectivity_factor: Annotated[
        bool, typer_option_apply_reflectivity_factor
    ] = True,
    solar_position_model: Annotated[
        SolarPositionModel, typer_option_solar_position_model
    ] = SOLAR_POSITION_ALGORITHM_DEFAULT,
    solar_incidence_model: Annotated[
        SolarIncidenceModel, typer_option_solar_incidence_model
    ] = SolarIncidenceModel.iqbal,
    solar_time_model: Annotated[
        SolarTimeModel, typer_option_solar_time_model
    ] = SOLAR_TIME_ALGORITHM_DEFAULT,
    solar_constant: Annotated[float, typer_option_solar_constant] = SOLAR_CONSTANT,
    eccentricity_phase_offset: Annotated[float, typer_option_eccentricity_phase_offset] = EccentricityPhaseOffset().value,
    eccentricity_amplitude: Annotated[
        float, typer_option_eccentricity_amplitude
    ] = EccentricityAmplitude().value,
    time_output_units: Annotated[str, typer_option_time_output_units] = MINUTES,
    angle_units: Annotated[str, typer_option_angle_units] = RADIANS,
    angle_output_units: Annotated[str, typer_option_angle_output_units] = RADIANS,
    # horizon_heights: Annotated[List[float], typer.Argument(help="Array of horizon elevations.")] = None,
    system_efficiency: Annotated[
        float | None, typer_option_system_efficiency
    ] = SYSTEM_EFFICIENCY_DEFAULT,
    power_model: Annotated[
        PhotovoltaicModulePerformanceModel, typer_option_pv_power_algorithm
    ] = PhotovoltaicModulePerformanceModel.king,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm, typer_option_module_temperature_algorithm
    ] = ModuleTemperatureAlgorithm.faiman,
    efficiency: Annotated[
        float | None, typer_option_efficiency
    ] = EFFICIENCY_FACTOR_DEFAULT,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
):
    """
    This method accounts for the effects of the solar spectrum's varying
    wavelengths on PV output, offering a more detailed analysis for systems
    sensitive to specific spectral ranges.
    """
    (
        spectrally_resolved_photovoltaic_power,
        results,
        title,
    ) = calculate_spectral_photovoltaic_power_output(
        longitude=longitude,
        latitude=latitude,
        elevation=elevation,
        timestamps=timestamps,
        timezone=timezone,
        spectrally_resolved_global_horizontal_irradiance_series=spectrally_resolved_global_horizontal_irradiance_series,
        spectrally_resolved_direct_horizontal_irradiance_series=spectrally_resolved_direct_horizontal_irradiance_series,
        spectral_response_data=spectral_response_data,
        number_of_junctions=number_of_junctions,
        standard_conditions_response=standard_conditions_response,
        minimum_spectral_mismatch=minimum_spectral_mismatch,
        temperature_series=temperature_series,
        wind_speed_series=wind_speed_series,
        mask_and_scale=mask_and_scale,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        in_memory=in_memory,
        surface_orientation=surface_orientation,
        surface_tilt=surface_tilt,
        linke_turbidity_factor_series=linke_turbidity_factor_series,
        adjust_for_atmospheric_refraction=adjust_for_atmospheric_refraction,
        unrefracted_solar_zenith=unrefracted_solar_zenith,
        albedo=albedo,
        apply_reflectivity_factor=apply_reflectivity_factor,
        solar_position_model=solar_position_model,
        solar_incidence_model=solar_incidence_model,
        solar_time_model=solar_time_model,
        solar_constant=solar_constant,
        eccentricity_phase_offset=eccentricity_phase_offset,
        eccentricity_amplitude=eccentricity_amplitude,
        time_output_units=time_output_units,
        angle_units=angle_units,
        angle_output_units=angle_output_units,
        system_efficiency=system_efficiency,
        power_model=power_model,
        temperature_model=temperature_model,
        efficiency=efficiency,
        verbose=verbose,
    )
    # longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)
    # latitude = convert_float_to_degrees_if_requested(latitude, angle_output_units)
    if not quiet:
        if verbose > 0:
            pass
        #     print_irradiance_table_2(
        #         longitude=longitude,
        #         latitude=latitude,
        #         timestamps=timestamps,
        #         dictionary=results,
        #         title=title + f' irradiance series {IRRADIANCE_UNIT}',
        #         rounding_places=rounding_places,
        #         index=index,
        #         verbose=verbose,
        #     )
        else:
            flat_list = spectrally_resolved_photovoltaic_power.flatten().astype(str)
            csv_str = ",".join(flat_list)
            print(csv_str)

temperature

Functions:

Name Description
photovoltaic_module_temperature

photovoltaic_module_temperature

photovoltaic_module_temperature(
    irradiance_series: Annotated[
        IrradianceSeries, typer_argument_irradiance_series
    ],
    temperature_series: Annotated[
        TemperatureSeries, typer_option_temperature_series
    ] = TEMPERATURE_DEFAULT,
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel,
        typer_option_photovoltaic_module_model,
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_option_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm,
        typer_option_module_temperature_algorithm,
    ] = faiman,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[
        bool, typer_option_command_metadata
    ] = False,
)
Source code in pvgisprototype/cli/power/temperature.py
@log_function_call
def photovoltaic_module_temperature(
    irradiance_series: Annotated[
        IrradianceSeries, typer_argument_irradiance_series
    ],
    temperature_series: Annotated[
        TemperatureSeries, typer_option_temperature_series
    ] = TEMPERATURE_DEFAULT,
    photovoltaic_module: Annotated[
        PhotovoltaicModuleModel, typer_option_photovoltaic_module_model
    ] = PHOTOVOLTAIC_MODULE_DEFAULT,
    wind_speed_series: Annotated[
        WindSpeedSeries, typer_option_wind_speed_series
    ] = WIND_SPEED_DEFAULT,
    temperature_model: Annotated[
        ModuleTemperatureAlgorithm, typer_option_module_temperature_algorithm
    ] = ModuleTemperatureAlgorithm.faiman,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    metadata: Annotated[bool, typer_option_command_metadata] = False,
):
    """
    """
    temperature_effect = calculate_photovoltaic_module_temperature_series(
        irradiance_series=irradiance_series,
        temperature_series=temperature_series,
        photovoltaic_module=photovoltaic_module,
        wind_speed_series=wind_speed_series,
        temperature_model=temperature_model,
        verbose=verbose,
    )

    return temperature_effect

print

Modules:

Name Description
caption
citation
fingerprint
flat
getters
helpers
hour_angle
irradiance
legend
metadata
panels
performance
position
qr
quantity
series
solar_time
sparklines
spectral_factor
surface
symbols
time
version
version_and_fingerprint

caption

Functions:

Name Description
build_caption

Build the main caption for a solar position tabular output.

build_simple_caption

Notes

build_caption

build_caption(
    data_dictionary,
    longitude,
    latitude,
    elevation=None,
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
    surface_orientation=True,
    surface_tilt=True,
    minimum_value=None,
    maximum_value=None,
)

Build the main caption for a solar position tabular output.

Include :

  • Location

    • Longitude ϑ
    • Latitude ϕ
    • Elevation + Unit
  • Position

    • Surface Orientation ↻
    • Surface Tilt ⦥
    • Angular units
Notes

Add the surface orientation and tilt only if they exist in the input data_dictionary !

Source code in pvgisprototype/cli/print/caption.py
def build_caption(
    data_dictionary,
    longitude,
    latitude,
    elevation=None,
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
    surface_orientation=True,
    surface_tilt=True,
    minimum_value=None,
    maximum_value=None,
):
    """Build the _main_ caption for a solar position tabular output.

    Include :

       - Location
         - Longitude ϑ
         - Latitude ϕ
         - Elevation + Unit

       - Position
         - Surface Orientation ↻
         - Surface Tilt ⦥
         - Angular units

    Notes
    -----
    Add the surface orientation and tilt only if they exist in the input
    `data_dictionary` !

    """
    # Collect items from `data_dictionary`
    first_model = next(iter(data_dictionary))
    data_dictionary = flatten_dictionary(data_dictionary[first_model])

    surface_orientation = round_float_values(
        (
            data_dictionary.get(SURFACE_ORIENTATION_COLUMN_NAME, None)
            if surface_orientation
            else ""
        ),
        rounding_places,
    )
    surface_tilt = round_float_values(
        (
            data_dictionary.get(SURFACE_TILT_COLUMN_NAME, None)
            if surface_tilt
            else None
        ),
        rounding_places,
    )
    angular_units = data_dictionary.get(ANGLE_UNITS_COLUMN_NAME, UNITLESS)

    # Build caption

    caption = str()

    # Location
    if longitude or latitude or elevation:
        caption += "[underline]Location[/underline]  "

    ## Longitude ϑ
    ## Latitude ϕ
    if longitude and latitude:
        caption += f"{LONGITUDE_COLUMN_NAME}, {LATITUDE_COLUMN_NAME} = [bold]{longitude}[/bold], [bold]{latitude}[/bold]"

    ## Angular units
    if (
        longitude
        or latitude
        or elevation
        or surface_orientation
        # or rear_side_surface_orientation
        or surface_tilt
        # or rear_side_surface_tilt
        and angular_units is not None
    ):
        caption += f"  [underline]Angular units[/underline] [dim][code]{angular_units}[/code][/dim]"

    ## Elevation + Unit
    if elevation:
        caption += f"{ELEVATION_COLUMN_NAME}: [bold]{elevation}[/bold]\n"

    # Position
    if (
        surface_orientation
        # or rear_side_surface_orientation
        or surface_tilt
        # or rear_side_surface_tilt
        and angular_units is not None
    ):
        caption += f"\n[underline]Position[/underline]  "

        ## Surface Orientation ↻
        if surface_orientation is not None:
            caption += (
                f"{SURFACE_ORIENTATION_COLUMN_NAME}: [bold]{surface_orientation}[/bold], "
            )
        ## Surface Tilt ⦥
        if surface_tilt is not None:
            caption += f"{SURFACE_TILT_COLUMN_NAME}: [bold]{surface_tilt}[/bold] "

    # What is this required for ? --------------------------------------------
    if minimum_value:
        caption += f"Minimum : {minimum_value}"

    if maximum_value:
        caption += f"Minimum : {maximum_value}"

    return caption

build_simple_caption

build_simple_caption(
    longitude,
    latitude,
    rounded_table,
    timezone,
    user_requested_timezone,
    minimum_value=None,
    maximum_value=None,
)
Notes

Add the surface orientation and tilt only if they exist in the input rounded_table !

Source code in pvgisprototype/cli/print/caption.py
def build_simple_caption(
    longitude,
    latitude,
    rounded_table,
    timezone,
    user_requested_timezone,
    minimum_value=None,
    maximum_value=None,
):
    """
    Notes
    -----
    Add the surface orientation and tilt only if they exist in the input
    `rounded_table` !

    """
    caption = (
        f"[underline]Position[/underline]  "
        + (
            f"Orientation : [bold blue]{rounded_table.get(SURFACE_ORIENTATION_NAME).value}[/bold blue], "
            if rounded_table.get(SURFACE_ORIENTATION_NAME) is not None
            else ""
        )
        + (
            f"Tilt : [bold blue]{rounded_table.get(SURFACE_TILT_NAME).value}[/bold blue] "
            if rounded_table.get(SURFACE_TILT_NAME) is not None
            else ""
        )
        + f"\n[underline]Location[/underline]  "
        + (
            f"{LONGITUDE_COLUMN_NAME}, {LATITUDE_COLUMN_NAME} = [bold]{longitude}[/bold], [bold]{latitude}[/bold], "
        )
        + f"[dim]{rounded_table.get(UNIT_NAME, UNITLESS)}[/dim]"
        f"\n[underline]{MEAN_PHOTOVOLTAIC_POWER_NAME}[/underline]  "
        + f"[bold blue]{rounded_table.get(MEAN_PHOTOVOLTAIC_POWER_NAME)}[/bold blue] "
        f"\n[underline]Algorithms[/underline]  "  # ---------------------------
        f"Timing : [bold]{rounded_table.get(TIME_ALGORITHM_NAME, NOT_AVAILABLE)}[/bold], "
    )

    if user_requested_timezone is not None and user_requested_timezone != ZoneInfo(
        "UTC"
    ):
        caption += f"Local Zone : [bold]{user_requested_timezone}[/bold], "
    else:
        caption += f"Zone : [bold]{timezone}[/bold], "

    if minimum_value:
        caption += f"Minimum : {minimum_value}"

    if maximum_value:
        caption += f"Minimum : {maximum_value}"

    return caption

citation

Functions:

Name Description
print_citation_text

print_citation_text

print_citation_text(
    bibtex: Annotated[
        bool,
        Option(help="Print citation in BibTeX format."),
    ] = False,
) -> None
Source code in pvgisprototype/cli/print/citation.py
def print_citation_text(
    bibtex: Annotated[bool, typer.Option(help="Print citation in BibTeX format.")] = False,
) -> None:
    """
    """
    citation = generate_citation_text(bibtex=bibtex)
    print(citation)

fingerprint

Functions:

Name Description
build_fingerprint_panel
print_finger_hash

Print the fingerprint if found, otherwise print a warning.

retrieve_fingerprint

Recursively search for the fingerprint key in a nested dictionary.

build_fingerprint_panel

build_fingerprint_panel(fingerprint) -> Panel
Source code in pvgisprototype/cli/print/fingerprint.py
def build_fingerprint_panel(fingerprint) -> Panel:
    """ """
    fingerprint = Text(
        fingerprint,
        justify="center",
        style="yellow bold",
    )
    return Panel(
        fingerprint,
        subtitle="[reverse]Fingerprint[/reverse]",
        subtitle_align="right",
        border_style="dim",
        style="dim",
        expand=False,
        padding=(0, 2),
    )

print_finger_hash

print_finger_hash(
    dictionary: dict,
    fingerprint_key: str = FINGERPRINT_COLUMN_NAME,
)

Print the fingerprint if found, otherwise print a warning.

Source code in pvgisprototype/cli/print/fingerprint.py
def print_finger_hash(
    dictionary: dict,
    fingerprint_key: str = FINGERPRINT_COLUMN_NAME,
):
    """Print the fingerprint if found, otherwise print a warning."""
    fingerprint = retrieve_fingerprint(
        dictionary,
        fingerprint_key,
    )
    if fingerprint is None:
        fingerprint = "No fingerprint found!"
        color = "red"
    else:
        color = "yellow"

    fingerprint_panel = Panel.fit(
        Text(f"{fingerprint}", justify="center", style=f"bold {color}"),
        subtitle="[reverse]Fingerprint[/reverse]",
        subtitle_align="right",
        border_style="dim",
        style="dim",
    )
    Console().print(fingerprint_panel)

retrieve_fingerprint

retrieve_fingerprint(
    dictionary: dict,
    fingerprint_key: str = FINGERPRINT_COLUMN_NAME,
) -> str | None

Recursively search for the fingerprint key in a nested dictionary.

Source code in pvgisprototype/cli/print/fingerprint.py
def retrieve_fingerprint(
    dictionary: dict, fingerprint_key: str = FINGERPRINT_COLUMN_NAME
) -> str | None:
    """
    Recursively search for the fingerprint key in a nested dictionary.
    """
    if isinstance(dictionary, dict):
        if fingerprint_key in dictionary:
            logger.info(f"Found the fingerprint key {fingerprint_key=}")
            return dictionary[fingerprint_key]

        # Recursively search each value of the dictionary
        for _, value in dictionary.items():
            fingerprint = retrieve_fingerprint(value)
            if fingerprint is not None:
                logger.info(f"Retrieved the fingerprint {fingerprint=}")
                return fingerprint

    logger.debug(f"Did not identify a fingerprint in the input data structure {dictionary=} !")
    return None

flat

Functions:

Name Description
flatten_dictionary

Flatten a nested dictionary

flatten_dictionary

flatten_dictionary(dictionary)

Flatten a nested dictionary

Parameters:

Name Type Description Default
dictionary

The nested dictionary to flatten

required

Returns:

Type Description
A flattened dictionary excluding the specified keys
Source code in pvgisprototype/cli/print/flat.py
def flatten_dictionary(dictionary):
    """
    Flatten a nested dictionary

    Parameters
    ----------
    dictionary: dict
        The nested dictionary to flatten

    Returns
    -------
    A flattened dictionary excluding the specified keys

    """
    flat_dictionary = {}

    def flatten(input_dictionary):
        for key, value in input_dictionary.items():

            if isinstance(value, dict):
                flatten(value)

            else:
                # Discard empty arrays
                if isinstance(value, ndarray):
                    if value.size == 0:
                        continue

                    # Discard arrays that are all NaN
                    elif (
                        issubclass(value.dtype.type, (float, int))
                        and isnan(value).all()
                    ):
                        continue

                    else:
                        flat_dictionary[key] = value
                else:
                    flat_dictionary[key] = value

    flatten(dictionary)
    return flat_dictionary

getters

Functions:

Name Description
get_event_time_value

Safely get the event time

get_scalar

Safely get a scalar value from an array or return the value itself

get_value_or_default

Get a value from a flat or nested dict using a string key or Enum.

get_event_time_value

get_event_time_value(dictionary, idx, rounding_places)

Safely get the event time

Source code in pvgisprototype/cli/print/getters.py
def get_event_time_value(
        dictionary,
        idx,
        rounding_places,
):
    """Safely get the event time """
    if dictionary is not None:
        event_time_series = get_value_or_default(
            dictionary=dictionary,
            key=SolarPositionParameter.event_time,
            default=None,
            )
        if event_time_series is not None and not (
            isinstance(event_time_series, DatetimeIndex) and
            all(isna(x) for x in event_time_series)
        ):
            return get_scalar(event_time_series, idx, rounding_places)
    else:
        return None

get_scalar

get_scalar(value, index, places)

Safely get a scalar value from an array or return the value itself

Source code in pvgisprototype/cli/print/getters.py
def get_scalar(value, index, places):
    """Safely get a scalar value from an array or return the value itself"""
    if isinstance(value, ndarray):
        if value.size > 1:
            return value[index]
        else:
            return value[0]

    return value

get_value_or_default

get_value_or_default(
    dictionary: dict,
    key: str | Enum,
    default: str | None = None,
)

Get a value from a flat or nested dict using a string key or Enum.

Source code in pvgisprototype/cli/print/getters.py
def get_value_or_default(
    dictionary: dict,
    key: str | Enum,
    default: str | None = None,
):
    """Get a value from a flat or nested dict using a string key or Enum."""
#     if dictionary is not None:
#         return dictionary.get(key, default)
#     else:
#         return None
    from enum import Enum

    if dictionary is None:
        return None

    # If key is an Enum, use its value
    if isinstance(key, Enum):
        key = key.value

    # 1) Try top level (flat dict)
    if key in dictionary:
        return dictionary[key]

    # 2) Try the 'Core' section (new nested structure)
    core = dictionary.get("Core")
    if isinstance(core, dict) and key in core:
        return core[key]

    # 3) Not found: return default
    return default

helpers

Functions:

Name Description
determine_frequency
infer_frequency_from_timestamps

Process timestamps to infer frequency based on regularity or irregularity of intervals.

determine_frequency

determine_frequency(timestamps)
Source code in pvgisprototype/cli/print/helpers.py
def determine_frequency(timestamps):
    """ """
    # single timestamp ?
    if len(timestamps) == 1:
        return "Single", "Single Timestamp"

    time_groupings = {
        "YE": "Yearly",
        "S": "Seasonal",
        "ME": "Monthly",
        "W": "Weekly",
        "D": "Daily",
        "3h": "3-Hourly",
        "h": "Hourly",
        "min": "Minutely",
        "8min": "8-Minutely",
    }
    if timestamps.year.unique().size > 1:
        frequency = "YE"
    elif timestamps.month.unique().size > 1:
        frequency = "ME"
    elif timestamps.to_period('W').week.unique().size > 1:
        frequency = "W"
    elif timestamps.day.unique().size > 1:
        frequency = "D"
    elif timestamps.hour.unique().size > 1:
        if timestamps.hour.unique().size < 17:  # Explain Me !
            frequency = "h"
        else:
            frequency = "3h"
    elif timestamps.minute.unique().size < 17:  # Explain Me !
        frequency = "min"
    else:
        frequency = "8min"  # by 8 characters for a sparkline if timestamps > 64 min
    frequency_label = time_groupings[frequency]

    return frequency, frequency_label

infer_frequency_from_timestamps

infer_frequency_from_timestamps(timestamps: DatetimeIndex)

Process timestamps to infer frequency based on regularity or irregularity of intervals.

Source code in pvgisprototype/cli/print/helpers.py
def infer_frequency_from_timestamps(timestamps: DatetimeIndex):
    """
    Process timestamps to infer frequency based on regularity or irregularity of intervals.
    """
    if timestamps.freqstr:  # timestamps are regular
        logger.debug(
            f"Regular intervals detected: {timestamps.freqstr}",
            alt=f"[bold]Regular intervals detected:[/bold] {timestamps.freqstr}",
        )
        return timestamps.freqstr, f"{timestamps.freqstr}"

    else:
        try:
            # Calculate time differences directly with NumPy for regular intervals
            time_deltas = numpy.diff(timestamps).astype("timedelta64[ns]")

            # Find the most frequent time delta using numpy.unique
            unique_deltas, counts = numpy.unique(time_deltas, return_counts=True)
            frequency = unique_deltas[numpy.argmax(counts)]
            logger.debug(
                f"Inferred frequency of timestamps: {frequency}",
                alt=f"[bold]Inferred frequency of timestamps:[/bold] {frequency}",
            )

            # Calculate the total duration : end - start
            total_duration = (timestamps[-1] - timestamps[0]).astype("timedelta64[ns]")

            # Calculate the number of intervals
            intervals = total_duration / frequency

            # Check if the number of intervals matches the number of timestamps - 1 (with tolerance)
            if numpy.isclose(len(timestamps) - 1, intervals, atol=1e-8):
                # If the intervals match, we can say the series is regular
                from pandas import to_timedelta

                return frequency, f"Regular intervals of {to_timedelta(frequency)}"

            else:
                try:
                    # Fallback to determine_frequency for irregular intervals
                    frequency, frequency_label = determine_frequency(timestamps)
                    logger.debug(
                        f"Categorized irregular frequency: {frequency_label}",
                        alt=f"[bold]Categorized irregular frequency:[/bold] {frequency_label}",
                    )

                    return frequency, frequency_label

                except Exception as e:
                    logger.error(f"Error in irregular frequency determination: {e}")
                    return None, "Error in determining irregular frequency"

        except Exception as e:
            logger.error(f"Error in regular frequency determination: {e}")
            return None, "Error in determining regular frequency"

hour_angle

Functions:

Name Description
print_hour_angle_table

print_hour_angle_table

print_hour_angle_table(
    latitude,
    rounding_places,
    surface_tilt=None,
    declination=None,
    hour_angle=None,
    units=None,
) -> None
Source code in pvgisprototype/cli/print/hour_angle.py
def print_hour_angle_table(
    latitude,
    rounding_places,
    surface_tilt=None,
    declination=None,
    hour_angle=None,
    units=None,
) -> None:
    """ """
    latitude = round_float_values(latitude, rounding_places)
    # rounded_table = round_float_values(table, rounding_places)
    surface_tilt = round_float_values(surface_tilt, rounding_places)
    declination = round_float_values(declination, rounding_places)
    hour_angle = round_float_values(hour_angle, rounding_places)

    columns = ["Latitude", "Event"]
    if surface_tilt is not None:
        columns.append(SURFACE_TILT_COLUMN_NAME)
    if declination is not None:
        columns.append(DECLINATION_COLUMN_NAME)
    if hour_angle is not None:
        columns.append(HOUR_ANGLE_COLUMN_NAME)
    columns.append(UNITS_COLUMN_NAME)

    table = Table(
        *columns,
        box=SIMPLE_HEAD,
        show_header=True,
        header_style="bold magenta",
    )

    row = [str(latitude), "Event"]
    if surface_tilt is not None:
        row.append(str(surface_tilt))
    if declination is not None:
        row.append(str(declination))
    if hour_angle is not None:
        row.append(str(hour_angle))
    row.append(str(units))
    table.add_row(*row)

    Console().print(table)

irradiance

Modules:

Name Description
caption
columns
data
table
text

caption

Functions:

Name Description
build_caption_for_irradiance_data
build_caption_for_irradiance_data
build_caption_for_irradiance_data(
    longitude=None,
    latitude=None,
    elevation=None,
    timezone: ZoneInfo | None = None,
    dictionary: dict = dict(),
    rear_side_irradiance_data: dict = dict(),
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
)
Source code in pvgisprototype/cli/print/irradiance/caption.py
def build_caption_for_irradiance_data(
    longitude=None,
    latitude=None,
    elevation=None,
    timezone: ZoneInfo | None = None,
    dictionary: dict = dict(),
    rear_side_irradiance_data: dict = dict(),
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
):
    """
    """
    caption = str()

    # Collect ----------------------------------------------------------------

    surface_orientation = round_float_values(
        dictionary.get(SolarSurfacePositionParameterColumnName.orientation, None),
        rounding_places,
    )
    surface_tilt = round_float_values(
        dictionary.get(SolarSurfacePositionParameterColumnName.tilt, None),
        rounding_places,
    )

    # Multiple solar surfaces ?
    surface_orientations = dictionary.get(
        SolarSurfacePositionParameterColumnName.orientations, None
    )
    surface_tilts = dictionary.get(SolarSurfacePositionParameterColumnName.tilts, None)

    # Units for both front-side and rear-side too !  Should _be_ the same !
    angular_units = dictionary.get(ANGLE_UNITS_COLUMN_NAME, UNITLESS)

    # Mainly about : Mono- or Bi-Facial ?
    # Maybe do the following :
    # If NOT rear_side_irradiance_data.get(PHOTOVOLTAIC_MODULE_TYPE_NAME)
    #     Then use the one from the dictionary which should be Monofacial
    # Else :
    #    Use the rear_side_irradiance_data which should be defined as Bifacial !
    photovoltaic_module_type = dictionary.get(PHOTOVOLTAIC_MODULE_TYPE_NAME, None)
    technology_name_and_type = dictionary.get(TECHNOLOGY_NAME, None)
    photovoltaic_module, mount_type = (
        technology_name_and_type.split(":")
        if technology_name_and_type
        else (None, None)
    )
    peak_power = str(dictionary.get(PEAK_POWER_COLUMN_NAME, None))
    peak_power += f' [dim]{dictionary.get(PEAK_POWER_UNIT_NAME, None)}[/dim]'

    algorithms = dictionary.get(POWER_MODEL_COLUMN_NAME, None)
    irradiance_data_source = dictionary.get(IRRADIANCE_SOURCE_COLUMN_NAME, None)
    radiation_model = dictionary.get(RADIATION_MODEL_COLUMN_NAME, None)
    equation = dictionary.get('Equation', None)

    timing_algorithm = dictionary.get(TIME_ALGORITHM_COLUMN_NAME, None)
    solar_positioning_algorithm = dictionary.get(POSITIONING_ALGORITHM_COLUMN_NAME, None)
    adjusted_for_atmospheric_refraction = dictionary.get('Unrefracted ⦧', None)
    azimuth_origin = dictionary.get(AZIMUTH_ORIGIN_COLUMN_NAME, None)
    ## Positions sun-to-horizon : from the set {['Above', 'Low angle', 'Below']}
    ## for which calculations were performed !
    if dictionary.get(SolarPositionParameterMetadataColumnName.sun_horizon_positions, None):
        sun_horizon_positions = [position.value for position in dictionary.get(SolarPositionParameterMetadataColumnName.sun_horizon_positions, None)]
    else:
        sun_horizon_positions = None

    incidence_algorithm = dictionary.get(INCIDENCE_ALGORITHM_COLUMN_NAME, None)
    shading_algorithm = dictionary.get(SHADING_ALGORITHM_COLUMN_NAME, None)

    if dictionary.get(SHADING_STATES_COLUMN_NAME) is not None:
        shading_states = [state.value for state in dictionary.get(SHADING_STATES_COLUMN_NAME, None)]
    else:
        shading_states = None

    # Review Me : What does and what does NOT make sense to have separately ?
    if rear_side_irradiance_data:
        rear_side_peak_power = str(dictionary.get(PEAK_POWER_COLUMN_NAME, None))
        rear_side_peak_power += f' [dim]{dictionary.get(PEAK_POWER_UNIT_NAME, None)}[/dim]'
        rear_side_algorithms = dictionary.get(POWER_MODEL_COLUMN_NAME, None)
    # ------------------------------------------------------------------------

    # Build the caption ------------------------------------------------------

    if longitude or latitude or elevation:
        caption += "[underline]Location[/underline]  "

    if longitude and latitude:
        caption += f"{LONGITUDE_COLUMN_NAME}, {LATITUDE_COLUMN_NAME} = [bold]{longitude}[/bold], [bold]{latitude}[/bold]"

    if elevation:
        caption += f", Elevation: [bold]{elevation} m[/bold]"


    if (
        surface_orientation
        and surface_tilt
        or surface_orientations
        and surface_tilts
        # val is not None
        # # for val in [surface_orientation, surface_tilt, rear_side_surface_orientation, rear_side_surface_tilt]
        # for val in [surface_orientation, surface_tilt]
    ):
        caption += "\n[underline]Position[/underline]  "

        if surface_orientation is not None:
            caption += f"{SURFACE_ORIENTATION_COLUMN_NAME}: [bold]{surface_orientation}[/bold], "

        if surface_tilt is not None:
            caption += f"{SURFACE_TILT_COLUMN_NAME}: [bold]{surface_tilt}[/bold] "

        if surface_orientations and surface_tilts:
            # Convert np.float64 to regular float, then format
            surface_orientations_string = ", ".join(
                [f"{float(val):.4f}" for val in surface_orientations]
            )
            caption += f"{SolarSurfacePositionParameterColumnName.orientations.value}: [bold]{surface_orientations_string}[/bold], "

            surface_tilts_string = ", ".join(
                [f"{float(val):.4f}" for val in surface_tilts]
            )
            caption += f"{SolarSurfacePositionParameterColumnName.tilts.value}: [bold]{surface_tilts_string}[/bold] "

    # Rear-side ?
    if rear_side_irradiance_data:

        rear_side_surface_orientation = round_float_values(
            rear_side_irradiance_data.get(REAR_SIDE_SURFACE_ORIENTATION_COLUMN_NAME, None), 
            rounding_places
        )
        if rear_side_surface_orientation is not None:
            caption += (
                f", {REAR_SIDE_SURFACE_ORIENTATION_COLUMN_NAME}: [bold]{rear_side_surface_orientation}[/bold], "
            )

        rear_side_surface_tilt = round_float_values(
            rear_side_irradiance_data.get(REAR_SIDE_SURFACE_TILT_COLUMN_NAME, None), 
            rounding_places
        )
        if rear_side_surface_tilt is not None:
            caption += f"{REAR_SIDE_SURFACE_TILT_COLUMN_NAME}: [bold]{rear_side_surface_tilt}[/bold] "

    # Units for both front-side and rear-side too !  Should _be_ the same !
    if (
        longitude
        or latitude
        or elevation
        or surface_orientation
        # or rear_side_surface_orientation
        or surface_tilt
        # or rear_side_surface_tilt
        and angular_units is not None
    ):
        caption += f"  [underline]Angular units[/underline] [dim][code]{angular_units}[/code][/dim]"


    # Photovoltaic Module

    if photovoltaic_module:
        caption += "\n[underline]Module[/underline]  "
        caption += f"Type: [bold]{photovoltaic_module_type}[/bold], "
        caption += f"{TECHNOLOGY_NAME}: [bold]{photovoltaic_module}[/bold], "
        caption += f"Mount: [bold]{mount_type}[/bold], "
        caption += f"{PEAK_POWER_COLUMN_NAME}: [bold]{peak_power}[/bold]"

    # Fundamental Definitions

    if (
        surface_orientation
        or surface_tilt
        or surface_orientations
        or surface_tilts
    ):
        caption += "\n[underline]Definitions[/underline]  "

        if azimuth_origin:
            caption += f"Azimuth origin : [bold blue]{azimuth_origin}[/bold blue], "

        if timezone:
            if timezone == ZoneInfo('UTC'):
                caption += f"[bold]{timezone}[/bold], "
            else:
                caption += f"Local Zone : [bold]{timezone}[/bold], "

        solar_incidence_definition = dictionary.get(INCIDENCE_DEFINITION_COLUMN_NAME, None)
        if solar_incidence_definition is not None:
            caption += f"{INCIDENCE_DEFINITION}: [bold yellow]{solar_incidence_definition}[/bold yellow], "

    solar_constant = dictionary.get(SOLAR_CONSTANT_COLUMN_NAME, None)
    eccentricity_phase_offset = dictionary.get(ECCENTRICITY_PHASE_OFFSET_COLUMN_NAME, None)
    eccentricity_amplitude = dictionary.get(
        ECCENTRICITY_AMPLITUDE_COLUMN_NAME, None
    )

    if sun_horizon_positions:
        caption += f"Sun-to-Horizon: [bold]{sun_horizon_positions}[/bold]"

    # Algorithms

    if (
        algorithms
        or radiation_model
        or timing_algorithm
        or solar_positioning_algorithm
        or incidence_algorithm
        or shading_algorithm
    ):
        caption += "\n[underline]Algorithms[/underline]  "

    if algorithms:
        caption += f"{POWER_MODEL_COLUMN_NAME}: [bold]{algorithms}[/bold], "

    if timing_algorithm:
        caption += f"Timing : [bold]{timing_algorithm}[/bold], "

    if solar_positioning_algorithm:
        caption += f"Positioning : [bold]{solar_positioning_algorithm}[/bold], "

    if adjusted_for_atmospheric_refraction:
        # caption += f"\n[underline]Atmospheric Properties[/underline]  "
        caption += f"Adjusted for refraction : [bold]{adjusted_for_atmospheric_refraction}[/bold], "

    if incidence_algorithm:
        caption += f"Incidence : [bold yellow]{incidence_algorithm}[/bold yellow], "

    if shading_algorithm:
        caption += f"Shading : [bold]{shading_algorithm}[/bold], "

    if shading_states:
        caption += f"Shading states : [bold]{shading_states}[/bold]"

    # if rear_side_shading_states:
    #     caption += f"Rear-side Shading states : [bold]{rear_side_shading_states}[/bold]"

    # Radiation model

    if radiation_model:
        caption += f"\n[underline]{RADIATION_MODEL_COLUMN_NAME}[/underline] : [bold]{radiation_model}[/bold], "
        irradiance_units = dictionary.get('Unit', UNITLESS)
        caption += f"[underline]Irradiance units[/underline] [dim]{irradiance_units}[/dim]"

        if equation:
            #from rich.markdown import Markdown
            #markdown_equation = Markdown(f"{equation}")
            caption += f"\nEquation : [dim][code]{equation}[/code][/dim]"


    # if rear_side_shading_algorithm:
    #     caption += f"Rear-side Shading : [bold]{rear_side_shading_algorithm}[/bold]"


    # solar_incidence_algorithm = dictionary.get(INCIDENCE_ALGORITHM_COLUMN_NAME, None)
    # if solar_incidence_algorithm is not None:
    #     caption += f"{INCIDENCE_ALGORITHM_COLUMN_NAME}: [bold yellow]{solar_incidence_algorithm}[/bold yellow], "

    if any([solar_constant, eccentricity_phase_offset, eccentricity_amplitude]):
        caption += "\n[underline]Constants[/underline] "
        if solar_constant:
            caption += f"{SOLAR_CONSTANT_COLUMN_NAME} : {solar_constant}, "

        if eccentricity_phase_offset and eccentricity_amplitude:
            caption += f"{ECCENTRICITY_PHASE_OFFSET_SHORT_COLUMN_NAME} : {eccentricity_phase_offset}, "
            caption += f"{ECCENTRICITY_AMPLITUDE_COLUMN_NAME} : {eccentricity_amplitude}, "

    # Sources ?

    if irradiance_data_source:
        caption += f"\n{IRRADIANCE_SOURCE_COLUMN_NAME}: [bold]{irradiance_data_source}[/bold], "

    return caption.rstrip(", ")  # Remove trailing comma + space

columns

Functions:

Name Description
add_key_table_columns

Notes

add_key_table_columns
add_key_table_columns(
    table,
    dictionary,
    timestamps,
    rounding_places,
    keys_to_sum: set = KEYS_TO_SUM,
    keys_to_average: set = KEYS_TO_AVERAGE,
    keys_to_exclude: set = KEYS_TO_EXCLUDE,
) -> RenderableType
Notes

Important : the input dictionary is expected to be a flat one.

Source code in pvgisprototype/cli/print/irradiance/columns.py
def add_key_table_columns(
    table,
    dictionary,
    timestamps,
    rounding_places,
    keys_to_sum: set = KEYS_TO_SUM,
    keys_to_average: set = KEYS_TO_AVERAGE,
    keys_to_exclude: set = KEYS_TO_EXCLUDE,
) -> RenderableType:
    """
    Notes
    -----
    Important : the input `dictionary` is expected to be a flat one.

    """
    for key, value in dictionary.items():
        if value is not None and key not in keys_to_exclude:

            # if single numeric or string, generate an array "of it" as long as the timestamps

            if isinstance(value, (float, int)):
                dictionary[key] = full(len(timestamps), value)

            if isinstance(value, str):
                dictionary[key] = full(len(timestamps), str(value))

            # add sum of value/s to the column footer

            if key in keys_to_sum:
                if (
                    isinstance(value, ndarray)
                    # and not isnan(value).all()
                    and value.dtype.kind in "if"
                ):
                    sum_of_key_value = Text(
                        str(round_float_values(nansum(value), rounding_places)),
                        # style="code purple",
                        style="bold purple",
                    )
                    table.add_column(
                        header=key,
                        footer=sum_of_key_value,  # Place the Sum in the footer
                        footer_style="white",
                        no_wrap=False,
                    )

            elif key in keys_to_average:
                if (
                    isinstance(value, ndarray) and value.dtype.kind in "if"
                ) | isinstance(value, float):
                    table.add_column(
                        header=key,
                        footer=Text(str(nanmean(value))),  # Mean of Key Value in the footer
                        footer_style="italic blue",
                        no_wrap=False,
                    )
            else:
                table.add_column(key, no_wrap=False)

    return table

data

Functions:

Name Description
flatten_dictionary

Flatten a nested dictionary

print_irradiance_table_2
flatten_dictionary
flatten_dictionary(dictionary)

Flatten a nested dictionary

Parameters:

Name Type Description Default
dictionary

The nested dictionary to flatten

required

Returns:

Type Description
A flattened dictionary excluding the specified keys
Source code in pvgisprototype/cli/print/irradiance/data.py
def flatten_dictionary(dictionary):
    """
    Flatten a nested dictionary

    Parameters
    ----------
    dictionary: dict
        The nested dictionary to flatten

    Returns
    -------
    A flattened dictionary excluding the specified keys

    """
    flat_dictionary = {}

    def flatten(input_dictionary):
        for key, value in input_dictionary.items():

            if isinstance(value, dict):
                flatten(value)

            else:
                # Discard empty arrays
                if isinstance(value, ndarray):
                    if value.size == 0:
                        continue

                    # Discard arrays that are all NaN
                    elif issubclass(value.dtype.type, (float, int)) and isnan(value).all():
                        continue 

                    else:
                        flat_dictionary[key] = value
                else:
                    flat_dictionary[key] = value

    flatten(dictionary)
    return flat_dictionary
print_irradiance_table_2
print_irradiance_table_2(
    title: str | None = "Power & Irradiance",
    irradiance_data: dict = dict(),
    rear_side_irradiance_data: dict = dict(),
    longitude=None,
    latitude=None,
    elevation=None,
    timestamps: Timestamp | DatetimeIndex = now(),
    user_requested_timestamps=None,
    timezone: ZoneInfo | None = None,
    user_requested_timezone=None,
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
    index: bool = False,
    verbose=1,
) -> None
Source code in pvgisprototype/cli/print/irradiance/data.py
def print_irradiance_table_2(
    title: str | None = "Power & Irradiance",
    irradiance_data: dict = dict(),
    rear_side_irradiance_data: dict = dict(),
    longitude=None,
    latitude=None,
    elevation=None,
    timestamps: Timestamp | DatetimeIndex = Timestamp.now(),
    user_requested_timestamps=None,
    timezone: ZoneInfo | None = None,
    user_requested_timezone=None,
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
    index: bool = False,
    verbose=1,
) -> None:
    """ """
    irradiance_data = flatten_dictionary(irradiance_data)

    # in case of multiple solar surfaces :
    # iterate over each pair of (surface_orientation, surface_tilt)

    surface_orientation = irradiance_data.get(SolarSurfacePositionParameter.surface_orientation, None)
    surface_tilt = irradiance_data.get(SolarSurfacePositionParameter.surface_tilt, None)

    for _pair, (_surface_orientation_value, _surface_tilt_value) in enumerate(
        zip([surface_orientation], [surface_tilt])
    ):
        longitude = round_float_values(longitude, rounding_places)
        latitude = round_float_values(latitude, rounding_places)
        elevation = round_float_values(elevation, 0)  # rounding_places)

        # Caption

        caption = build_caption_for_irradiance_data(
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            timezone=timezone,
            dictionary=irradiance_data,
            rear_side_irradiance_data=rear_side_irradiance_data,
            rounding_places=rounding_places,
        )

        # then : create a Legend table for the symbols in question

        legend = build_legend_table(
            dictionary=irradiance_data,
            caption=caption,
            show_sum=True,
            show_mean=True,
            show_header=False,
            box=None,
        )

        # Define the time column name based on the timezone or user requests

        time_column_name = TIME_COLUMN_NAME if user_requested_timestamps is None else LOCAL_TIME_COLUMN_NAME

        # Build the irradiance table/s

        table = build_irradiance_table(
            title=title,
            index=index,
            dictionary=irradiance_data,
            timestamps=timestamps,
            rounding_places=rounding_places,
            time_column_name=time_column_name,
            time_column_footer=f"{SYMBOL_SUMMATION} / [blue]{SYMBOL_MEAN}[/blue]",  # Abusing this "cell" as a "Row Name" 
            time_column_footer_style = "purple",  # to make it somehow distinct from the Column !
            keys_to_sum = KEYS_TO_SUM,
            keys_to_average = KEYS_TO_AVERAGE,
            keys_to_exclude = KEYS_TO_EXCLUDE,
        )

        if rear_side_irradiance_data:
            rear_side_table = build_irradiance_table(
                title=f'Rear-side {title}',
                index=index,
                dictionary=irradiance_data,
                timestamps=timestamps,
                rounding_places=rounding_places,
                time_column_name=time_column_name,
                time_column_footer=f"{SYMBOL_SUMMATION} / [blue]{SYMBOL_MEAN}[/blue]",
                time_column_footer_style = "purple",
                keys_to_sum = REAR_SIDE_KEYS_TO_SUM,
                keys_to_average = KEYS_TO_AVERAGE,
                keys_to_exclude = KEYS_TO_EXCLUDE,
            )
            # totals_table = Table(
            #     title=f'Total {title}',
            #     # caption=caption.rstrip(', '),  # Remove trailing comma + space
            #     caption_justify="left",
            #     expand=False,
            #     padding=(0, 1),
            #     box=SIMPLE_HEAD,
            #     header_style="bold gray50",
            #     show_footer=True,
            #     footer_style='white',
            #     row_styles=["none", "dim"],
            #     highlight=True,
            # )
            # if index:
                # totals_table.add_column("")
            # totals_table.add_column("")
        else:
            rear_side_table = None  # in order to avoid the "unbound error"
            # totals_table = None

        # Populate table/s

        table = populate_irradiance_table(
                            table=table,
                            dictionary=irradiance_data,
                            timestamps=timestamps,
                            index=index,
                            rounding_places=rounding_places,
                            keys_to_exclude=KEYS_TO_EXCLUDE,
                        )

        if rear_side_table:
            rear_side_table = populate_irradiance_table(
                                table=rear_side_table,
                                dictionary=rear_side_irradiance_data,
                                timestamps=timestamps,
                                index=index,
                                rounding_places=rounding_places,
                            )

        # Print if requested via at least 1x `-v`

        if verbose:
            print_table_and_legend(
                caption=caption,
                table=table,
                rear_side_table=rear_side_table,
                legend=legend,
            )

table

Functions:

Name Description
build_irradiance_table
populate_irradiance_table
print_irradiance_xarray

Print the irradiance time series in a formatted table with each center wavelength as a column.

print_table_and_legend

Print panels for both caption and legend

build_irradiance_table
build_irradiance_table(
    title: str | None,
    index: bool,
    dictionary,
    timestamps,
    rounding_places,
    keys_to_sum: dict,
    keys_to_average: dict,
    keys_to_exclude: dict,
    time_column_name: RenderableType = "Time",
    time_column_footer: RenderableType = SYMBOL_SUMMATION,
    time_column_footer_style: str = "purple",
) -> RenderableType
Source code in pvgisprototype/cli/print/irradiance/table.py
def build_irradiance_table(
    title: str | None,
    index: bool,
    dictionary,
    timestamps,
    rounding_places,
    keys_to_sum: dict,
    keys_to_average: dict,
    keys_to_exclude: dict,
    time_column_name: RenderableType = "Time",
    time_column_footer: RenderableType = SYMBOL_SUMMATION,
    time_column_footer_style: str = "purple",
) -> RenderableType:
    """
    """
    table = Table(
        title=title,
        # caption=caption.rstrip(', '),  # Remove trailing comma + space
        caption_justify="left",
        expand=False,
        padding=(0, 1),
        box=SIMPLE_HEAD,
        header_style="bold gray50",
        show_footer=True,
        footer_style='white',
        row_styles=["none", "dim"],
        highlight=True,
    )

    # base columns

    if index:
        table.add_column("Index")

    ## Time column

    table.add_column(
        time_column_name,
        no_wrap=True,
        footer=time_column_footer,
        footer_style=time_column_footer_style,
    )

    # remove the 'Title' entry! ---------------------------------------------
    dictionary.pop("Title", NOT_AVAILABLE)
    # ------------------------------------------------------------- Important

    # add and process additional columns

    table = add_key_table_columns(
                        table=table,
                        dictionary=dictionary,
                        timestamps=timestamps,
                        rounding_places=rounding_places,
                        keys_to_sum=keys_to_sum,
                        keys_to_average=keys_to_average,
                        keys_to_exclude=keys_to_exclude,
                    )

    return table
populate_irradiance_table
populate_irradiance_table(
    table,
    dictionary,
    timestamps,
    index,
    rounding_places,
    keys_to_exclude: set = KEYS_TO_EXCLUDE,
) -> RenderableType
Source code in pvgisprototype/cli/print/irradiance/table.py
def populate_irradiance_table(
    table,
    dictionary,
    timestamps,
    index,
    rounding_places,
    keys_to_exclude: set = KEYS_TO_EXCLUDE,
) -> RenderableType:
    """
    """
    # Zip series and timestamps
    filtered_dictionary = {
        key: numpy.atleast_1d(value) for key, value in dictionary.items()
        if key not in keys_to_exclude and value is not None
    }

    none_keys = [key for key, value in filtered_dictionary.items() if value is None]
    if none_keys:
        raise ValueError(f"The following keys are of `NoneType` which is not iterable and thus cannot be zipped: {none_keys}")
    zipped_series = zip(*filtered_dictionary.values())
    zipped_data = zip(timestamps, zipped_series)

    index_counter = 1
    for timestamp, values in zipped_data:
        row = []

        if index:
            row.append(str(index_counter))
            index_counter += 1

        row.append(to_datetime(timestamp).strftime("%Y-%m-%d %H:%M:%S"))

        for idx, (column_name, value) in enumerate(
            zip(filtered_dictionary.keys(), values)
        ):
            # First row of the table is the header
            if idx == 0:  # assuming after 'Time' is the value of main interest
                # Make first row item bold
                bold_value = Text(
                    str(round_float_values(value, rounding_places)), style="bold dark_orange",
                )
                row.append(bold_value)

            else:
                # if not isinstance(value, str) or isinstance(value, float):

                row.append(
                    format_string(
                        value=value,
                        column_name=column_name,
                        rounding_places=rounding_places,
                    )
                )

        table.add_row(*row)

    return table
print_irradiance_xarray
print_irradiance_xarray(
    location_time_series: DataArray,
    longitude=None,
    latitude=None,
    elevation=None,
    title: str | None = "Irradiance data",
    rounding_places: int = 3,
    verbose: int = 1,
    index: bool = False,
) -> None

Print the irradiance time series in a formatted table with each center wavelength as a column.

Parameters:

Name Type Description Default
location_time_series DataArray

The time series data with dimensions (time, center_wavelength).

required
longitude float

The longitude of the location.

None
latitude float

The latitude of the location.

None
elevation float

The elevation of the location.

None
title str

The title of the table.

'Irradiance data'
rounding_places int

The number of decimal places to round to.

3
verbose int

Verbosity level.

1
index bool

Whether to show an index column.

False
Source code in pvgisprototype/cli/print/irradiance/table.py
def print_irradiance_xarray(
    location_time_series: DataArray,
    longitude=None,
    latitude=None,
    elevation=None,
    # coordinate: str = None,
    title: str | None = "Irradiance data",
    rounding_places: int = 3,
    verbose: int = 1,
    index: bool = False,
) -> None:
    """
    Print the irradiance time series in a formatted table with each center wavelength as a column.

    Parameters
    ----------
    location_time_series : xr.DataArray
        The time series data with dimensions (time, center_wavelength).
    longitude : float, optional
        The longitude of the location.
    latitude : float, optional
        The latitude of the location.
    elevation : float, optional
        The elevation of the location.
    title : str, optional
        The title of the table.
    rounding_places : int, optional
        The number of decimal places to round to.
    verbose : int, optional
        Verbosity level.
    index : bool, optional
        Whether to show an index column.
    """
    console = Console()
    # Extract relevant data from the location_time_series

    # Prepare the table
    table = Table(
        title=title,
        caption_justify="left",
        expand=False,
        padding=(0, 1),
        box=SIMPLE_HEAD,
        show_footer=True,
    )

    if index:
        table.add_column("Index")

    if 'time' in location_time_series.dims:
        table.add_column("Time", footer=f"{SYMBOL_SUMMATION}")

    # Add columns for each center wavelength (irradiance wavelength)
    if 'center_wavelength' in location_time_series.coords:
        center_wavelengths = location_time_series.coords['center_wavelength'].values
        if center_wavelengths.size > 0:
            for wavelength in center_wavelengths:
                table.add_column(f"{wavelength:.0f} nm", justify="right")
        else:
            logger.warning("No center_wavelengths found in the dataset.")
    else:
        logger.warning("No 'center_wavelength' coordinate found in the dataset.")

    # Populate the table with the irradiance data

    # case of scalar data
    if 'time' not in location_time_series.dims:
        row = []
        if index:
            row.append("1")  # Single row for scalar data

        # Handle the presence of a coordinate (like center_wavelength)
        # if coordinate ... ?
        if 'center_wavelength' in location_time_series.coords:
            for irradiance_value in location_time_series.values:
                row.append(f"{round(irradiance_value, rounding_places):.{rounding_places}f}")
        else:
            row.append(f"{round(location_time_series.item(), rounding_places):.{rounding_places}f}")

        table.add_row(*row)


    else:
        irradiance_values = location_time_series.values
        for idx, timestamp in enumerate(location_time_series.time.values):
            row = []

            if index:
                row.append(str(idx + 1))

            # Convert timestamp to string format
            try:
                row.append(to_datetime(timestamp).strftime("%Y-%m-%d %H:%M:%S"))
            except Exception as e:
                logger.error(f"Invalid timestamp at index {idx}: {e}")
                row.append("Invalid timestamp")

            if 'center_wavelength' in location_time_series.coords:
                # add data variable values for each "coordinate" value at this timestamp
                # i.e. : irradiance values for each "center_wavelength" at this timestamp
                for irradiance_value in irradiance_values[idx]:
                    row.append(f"{round(irradiance_value, rounding_places):.{rounding_places}f}")
            else:
                #  a scalar, i.e. no Xarray "coordinate"
                row.append(f"{round(irradiance_values[idx], rounding_places):.{rounding_places}f}")

            table.add_row(*row)

    # Prepare a caption with the location information
    caption = str()
    if longitude is not None and latitude is not None:
        caption += f"Location  Longitude ϑ, Latitude ϕ = {longitude}, {latitude}"

    if elevation is not None:
        caption += f", Elevation: {elevation} m"

    caption += "\nLegend: Center Wavelengths (nm)"

    if verbose:
        console.print(table)
        console.print(Panel(caption, expand=False))
print_table_and_legend
print_table_and_legend(
    caption: RenderableType,
    table: RenderableType,
    rear_side_table: RenderableType | None,
    legend: RenderableType,
    caption_subtitle: str = "Reference",
    legend_subtitle: str = "Legend",
) -> None

Print panels for both caption and legend

Source code in pvgisprototype/cli/print/irradiance/table.py
def print_table_and_legend(
    caption: RenderableType,
    table: RenderableType,
    rear_side_table: RenderableType | None,
    legend: RenderableType,
    caption_subtitle: str = 'Reference',
    legend_subtitle: str = 'Legend',
) -> None:
    """
    Print panels for both caption and legend

    """
    Console().print(table)
    if rear_side_table:
        Console().print(rear_side_table)

    panels = []
    if caption:
        caption_panel = Panel(
            caption,
            subtitle=f"[gray]{caption_subtitle}[/gray]",
            subtitle_align="right",
            border_style="dim",
            expand=False
        )
        panels.append(caption_panel)

    if legend:
        legend_panel = Panel(
            legend,
            subtitle=f"[dim]{legend_subtitle}[/dim]",
            subtitle_align="right",
            border_style="dim",
            expand=False,
            padding=(0,1),
            # style="dim",
        )
        panels.append(legend_panel)

    # Use Columns to place them side-by-side
    from rich.columns import Columns
    Console().print(Columns(panels))

text

Functions:

Name Description
format_string
format_string
format_string(
    value: str | int | float | Style,
    column_name=None,
    rounding_places=ROUNDING_PLACES_DEFAULT,
)
Source code in pvgisprototype/cli/print/irradiance/text.py
def format_string(
    value: str | int | float | Style,
    # enum_model: EnumType,
    column_name=None,
    rounding_places=ROUNDING_PLACES_DEFAULT,
):
    """
    """
    # Handle None
    if value is None:
        return ""

    if isinstance(value, (EnumType, str)):

        if value in [string.value for string in ShadingState]:
            style = SHADING_STATE_COLOR_MAP.get(value, None)
            return Text(value, style=style)

        # Handle SolarEvent
        if value in [string.value for string in SolarEvent]:
            style = SOLAR_EVENT_COLOR_MAP.get(value, None)
            return Text(value, style=style)

        # Handle SunHorizonPositionModel
        if value in [string.value for string in SunHorizonPositionModel]:
            style = str(SUN_HORIZON_POSITION_COLOR_MAP.get(str(value)))
            return Text(value, style=style)

    # Handle negative numbers or loss columns
    else:
        rounded_value = round_float_values(value, rounding_places)

        # If values of this column are negative / represent loss
        if (
            f" {SYMBOL_LOSS}" in column_name
            or f"{SYMBOL_REFLECTIVITY}" in column_name
            or value < 0
        ):  # Avoid matching any `-`

            # Make them bold red
            return Text(str(rounded_value), style="bold red")

        elif isinstance(value, (numpy.number, float)) and value ==0.:
            return Text(str(rounded_value), style="dim gray")

        elif (isinstance(value, float) and (math.isnan(value))) or (
            isinstance(value, numpy.floating) and numpy.isnan(value)
        ):
            return Text(str(value), style="dim red")


        else:
            return str(rounded_value)

    # Fallback: just return as string
    return str(value)

legend

Functions:

Name Description
build_legend_table

build_legend_table

build_legend_table(
    dictionary: dict,
    caption: str,
    show_sum: bool = False,
    show_mean: bool = False,
    show_header: bool = False,
    box: str | None = None,
)
Source code in pvgisprototype/cli/print/legend.py
def build_legend_table(
    dictionary: dict,
    caption: str,
    show_sum: bool = False,
    show_mean: bool = False,
    show_header: bool = False,
    box: str | None = None,  # box=SIMPLE_HEAD,
):
    """
    """
    # from rich import print
    # for key, value in dictionary.items():
    #     print(f"{key=} : {value=}")

    # none_keys = [key for key, value in dictionary.items() if value is None]
    # if none_keys:
    #     raise ValueError(f"The following keys are of `NoneType` which is not iterable and thus cannot be zipped: {none_keys}")

    # first : Identify symbols in the input dictionary
    filtered_symbols = {
        symbol: description
        for symbol, description in SYMBOL_DESCRIPTIONS.items()
        if any(symbol in key for key in dictionary.keys())
    }

    # Check for SYMBOL_SUMMATION in the input dictionary before adding
    if show_sum or any(SYMBOL_SUMMATION in key for key in dictionary.keys()):
        filtered_symbols[SYMBOL_SUMMATION] = f"[purple]{SYMBOL_SUMMATION_NAME}[/purple]"

    # Check for SYMBOL_MEAN in the input dictionary before adding
    if show_mean or any(SYMBOL_MEAN in key for key in dictionary.keys()):
        filtered_symbols[SYMBOL_MEAN] = f"[blue]{SYMBOL_MEAN_NAME}[/blue]"

    # then : Create a Legend table for the symbols in question
    legend = Table(
        # title='Legend',
        # title="[code]Legend[/code]",
        # caption="Caption text",
        show_header=show_header,
        # header_style="dim",
        # row_styles=["none", "dim"],
        box=box,
        # highlight=True,
        # pad_edge=False,
        # collapse_padding=True,
    )

    # next : Determine the number of columns based on the "height" of Caption 
    if len(caption.splitlines()) == 0:
        return None
    else:
        number_of_symbols = len(filtered_symbols)
        number_of_rows = len(caption.splitlines())
        number_of_columns = ceil(number_of_symbols / number_of_rows) * 2  # Multiply by 2 for Symbol & Description pairs
        for _ in range(number_of_columns // 2):
            legend.add_column("Symbol", justify="center", style="bold blue", no_wrap=True)
            legend.add_column("Description", justify="left", style="dim", no_wrap=False)

        # finally : Populate the Legend table row by row
        rows = [["" for _ in range(number_of_columns)] for _ in range(number_of_rows)]  # Empty table grid
        current_row = 0  # Start with the first row
        current_column = 0  # Start with the first column pair

        for symbol, description in filtered_symbols.items():
            rows[current_row][current_column * 2] = f"[yellow]{symbol}[/yellow]"  # Symbol column
            if description == SYMBOL_POWER_NAME:
                rows[current_row][current_column * 2 + 1] = f"[dark_orange]{description}[/dark_orange]"  # Description column
            elif description == SYMBOL_LOSS_NAME:
                rows[current_row][current_column * 2 + 1] = f"[red bold]{description}[/red bold]"  # Description column
            else:
                rows[current_row][current_column * 2 + 1] = description  # Description column

            current_row += 1
            if current_row >= number_of_rows:  # Move to the next column if rows are filled
                current_row = 0
                current_column += 1

        # Add rows to the legend table
        for row in rows:
            legend.add_row(*row)

        return legend

metadata

Functions:

Name Description
print_command_metadata

print_command_metadata

print_command_metadata(context: Context)
Source code in pvgisprototype/cli/print/metadata.py
def print_command_metadata(context: Context):
    """ """
    command_parameters = {}
    command_parameters["command"] = context.command_path
    command_parameters = command_parameters | context.params
    command_parameters_panel = Panel.fit(
        Pretty(command_parameters, no_wrap=True),
        subtitle="[reverse]Command Metadata[/reverse]",
        subtitle_align="right",
        border_style="dim",
        style="dim",
    )
    Console().print(command_parameters_panel)

    # write to file ?
    import json

    from pvgisprototype.validation.serialisation import CustomEncoder

    with open("command_parameters.json", "w") as json_file:
        json.dump(command_parameters, json_file, cls=CustomEncoder, indent=4)

panels

Functions:

Name Description
build_version_and_fingerprint_columns

Combine software version and fingerprint panels into a single Columns

build_version_and_fingerprint_panels

Dynamically build panels based on available data.

build_version_and_fingerprint_columns

build_version_and_fingerprint_columns(
    version: bool = False, fingerprint: bool = False
) -> Columns

Combine software version and fingerprint panels into a single Columns object.

Source code in pvgisprototype/cli/print/panels.py
def build_version_and_fingerprint_columns(
    version:bool = False,
    fingerprint: bool = False,
) -> Columns:
    """Combine software version and fingerprint panels into a single Columns
    object."""
    version_and_fingeprint_panels = build_version_and_fingerprint_panels(
        version=version,
        fingerprint=fingerprint,
    )

    return Columns(version_and_fingeprint_panels, expand=False, padding=2)

build_version_and_fingerprint_panels

build_version_and_fingerprint_panels(
    version: bool = False, fingerprint: bool = False
) -> list[Panel]

Dynamically build panels based on available data.

Source code in pvgisprototype/cli/print/panels.py
def build_version_and_fingerprint_panels(
    version:bool = False,
    fingerprint: bool = False,
) -> list[Panel]:
    """Dynamically build panels based on available data."""
    # Always yield version panel
    panels = []
    if version:
        panels.append(build_pvgis_version_panel())
    # Yield fingerprint panel only if fingerprint is provided
    if fingerprint:
        panels.append(build_fingerprint_panel(fingerprint))

    return panels

performance

Modules:

Name Description
analysis
horizon
metadata
photovoltaic_module
position
table

analysis

Functions:

Name Description
print_change_percentages_panel

Print a formatted table of photovoltaic performance metrics using the

print_change_percentages_panel
print_change_percentages_panel(
    photovoltaic_power: PhotovoltaicPower,
    title: str = "Analysis of Performance",
    longitude=None,
    latitude=None,
    elevation=None,
    surface_orientation: float | bool = True,
    surface_tilt: float | bool = True,
    horizon_profile: NDArray | None = None,
    timestamps: DatetimeIndex | None = None,
    timezone: ZoneInfo | None = None,
    angle_output_units: str = RADIANS,
    rounding_places: int = 1,
    verbose=1,
    index: bool = False,
    version: bool = False,
    fingerprint: bool = False,
    quantity_style="magenta",
    value_style="cyan",
    unit_style="cyan",
    percentage_style="dim",
    reference_quantity_style="white",
)

Print a formatted table of photovoltaic performance metrics using the Rich library.

Analyse the photovoltaic performance in terms of :

  • In-plane (or inclined) irradiance without reflectivity loss
  • Reflectivity effect as a function of the solar incidence angle
  • Irradiance after reflectivity effect
  • Spectral effect due to variation in the natural sunlight spectrum and its difference to standardised artificial laborary light spectrum
  • Effective irradiance = Inclined irradiance + Reflectivity effect + Spectral effect
  • Loss as a function of the PV module temperature and low irradiance effects
  • Conversion of the effective irradiance to photovoltaic power
  • Total net effect = Reflectivity, Spectral effect, Temperature & Low irradiance

Finally, report the photovoltaic power output after system loss and degradation with age

Source code in pvgisprototype/cli/print/performance/analysis.py
def print_change_percentages_panel(
    # dictionary: dict = dict(),
    photovoltaic_power: PhotovoltaicPower,
    title: str = "Analysis of Performance",
    longitude=None,
    latitude=None,
    elevation=None,
    surface_orientation: float | bool = True,
    surface_tilt: float | bool = True,
    horizon_profile: NDArray | None = None,
    timestamps: DatetimeIndex | None = None,
    timezone: ZoneInfo | None = None,
    angle_output_units: str = RADIANS,
    rounding_places: int = 1,  # ROUNDING_PLACES_DEFAULT,
    verbose=1,
    index: bool = False,
    version: bool = False,
    fingerprint: bool = False,
    quantity_style="magenta",
    value_style="cyan",
    unit_style="cyan",
    percentage_style="dim",
    reference_quantity_style="white",
):
    """Print a formatted table of photovoltaic performance metrics using the
    Rich library.

    Analyse the photovoltaic performance in terms of :

    - In-plane (or inclined) irradiance without reflectivity loss
    - Reflectivity effect as a function of the solar incidence angle
    - Irradiance after reflectivity effect
    - Spectral effect due to variation in the natural sunlight spectrum and its
      difference to standardised artificial laborary light spectrum
    - Effective irradiance = Inclined irradiance + Reflectivity effect + Spectral effect
    - Loss as a function of the PV module temperature and low irradiance effects
    - Conversion of the effective irradiance to photovoltaic power
    - Total net effect = Reflectivity, Spectral effect, Temperature & Low
      irradiance

    Finally, report the photovoltaic power output after system loss and
    degradation with age

    """
    frequency, frequency_label = determine_frequency(timestamps=timestamps)
    add_empty_row_before = {
        # IN_PLANE_IRRADIANCE,
        REFLECTIVITY,
        # IRRADIANCE_AFTER_REFLECTIVITY,
        SPECTRAL_EFFECT_NAME,
        # EFFECTIVE_IRRADIANCE_NAME,
        TEMPERATURE_AND_LOW_IRRADIANCE_COLUMN_NAME,
        # PHOTOVOLTAIC_POWER_WITHOUT_SYSTEM_LOSS_COLUMN_NAME,
        SYSTEM_LOSS,
        # PHOTOVOLTAIC_POWER_LONG_NAME,
        # f"[white dim]{POWER_NAME}",
        f"[green bold]{ENERGY_NAME_WITH_SYMBOL}",
        # f"[white dim]{NET_EFFECT}",
    }
    performance_table = build_performance_table(
        frequency_label=frequency_label,
        quantity_style=quantity_style,
        value_style=value_style,
        unit_style=unit_style,
        mean_value_unit_style="white dim",
        percentage_style=percentage_style,
        # reference_quantity_style=reference_quantity_style,
    )
    results = report_photovoltaic_performance(
        dictionary=photovoltaic_power,
        timestamps=timestamps,
        frequency=frequency,
        verbose=verbose,
    )

    # Add rows based on the dictionary keys and corresponding values
    for label, (
        (value, value_style),
        (unit, unit_style),
        (mean_value, mean_value_style),
        (mean_value_unit, mean_value_unit_style),
        standard_deviation,
        percentage,
        style,
        reference_quantity,
        input_series,
        source,
    ) in results.items():
        if label in add_empty_row_before:
            performance_table.add_row()
        add_table_row(
            table=performance_table,
            quantity=label,
            value=value,
            unit=unit,
            mean_value=mean_value,
            mean_value_unit=mean_value_unit,
            standard_deviation=standard_deviation,
            percentage=percentage,
            reference_quantity=reference_quantity,
            series=input_series,
            timestamps=timestamps,
            frequency=frequency,
            source=source,
            quantity_style=quantity_style,
            value_style=value_style,
            unit_style=unit_style,
            mean_value_style=mean_value_style,
            mean_value_unit_style=mean_value_unit_style,
            percentage_style=percentage_style,
            reference_quantity_style=reference_quantity_style,
            rounding_places=rounding_places,
        )

    # Positioning 

    position_table = build_position_table()
    positioning_rounding_places = 3
    position_table = populate_position_table(
        table=position_table,
        data_model=photovoltaic_power,
        latitude=latitude,
        longitude=longitude,
        elevation=elevation,
        surface_orientation=surface_orientation,
        surface_tilt=surface_tilt,
        rounding_places=positioning_rounding_places,
    )
    position_panel = build_position_panel(position_table, width=performance_table.width)


    # Algorithmic metadata panel

    algorithmic_metadata_table = populate_algorithmic_metadata_table(
        data_model=photovoltaic_power
    )
    algorithmic_metadata_panel = build_algorithmic_metadata_panel(
        algorithmic_metadata_table
    )

    # Horizon profile

    if horizon_profile is not None:
        horizon_profile_polar_plot = generate_horizon_profile_polar_plot(horizon_profile)
        # horizon_profile_table = build_horizon_profile_table()
        # horizon_profile_table.add_row(
            # # f"{horizon_profile_polar_plot}",
            # horizon_profile_polar_plot
        # )
        horizon_profile_panel = build_horizon_profile_panel(
            # horizon_profile_table
            horizon_profile_polar_plot
        )
    else:
        horizon_profile_panel = None

    if algorithmic_metadata_panel and horizon_profile_panel is not None:
        metadata_columns = Columns([
            algorithmic_metadata_panel,
            horizon_profile_panel,
            ])
    else:
        metadata_columns = None

    # Timing

    time_table = build_time_table()
    time_table = populate_time_table(
        table=time_table, timestamps=timestamps, timezone=timezone
    )
    time_panel = build_time_panel(time_table)


    # Photovoltaic Module

    photovoltaic_module_table = build_photovoltaic_module_table()
    photovoltaic_module_table = populate_photovoltaic_module_table(
        table=photovoltaic_module_table,
        photovoltaic_power=photovoltaic_power,
    )
    photovoltaic_module_panel = build_photovoltaic_module_panel(
        photovoltaic_module_table
    )
    # panels = [position_panel, time_panel, photovoltaic_module_panel]

    # columns = Columns(
    #         panels,
    #         # expand=True,
    #         # equal=True,
    #         padding=2,
    #         )

    performance_panel = Panel(
        performance_table,
        title=title,
        expand=False,
        # style="on black",
    )
    photovoltaic_module_columns = Columns(
        [
            panel
            for panel in [
                position_panel,
                time_panel,
                photovoltaic_module_panel,
            ]
            if panel
        ],
        # expand=True,
        # equal=True,
        padding=3,
    )

    fingerprint = retrieve_fingerprint(dictionary=photovoltaic_power.output)
    version_and_fingerprint_columns = build_version_and_fingerprint_columns(
        version=version,
        fingerprint=fingerprint,
    )

    from rich.console import Group

    group_panels = [
        panel
        for panel in [
            photovoltaic_module_columns,
            performance_panel,
            # algorithmic_metadata_panel,
            metadata_columns,
            # horizon_profile_panel,
            version_and_fingerprint_columns,
        ]
        if panel is not None
    ]
    group = Group(*group_panels)

    # panel_group = Group(
    #         Panel(
    #             performance_table,
    #             title='Analysis of Performance',
    #             expand=False,
    #             # style="on black",
    #             ),
    #         columns,
    #     # Panel(table),
    #     # Panel(position_panel),
    # #     Panel("World", style="on red"),
    #         fit=False
    # )

    # Console().print(panel_group)
    # Console().print(Panel(performance_table))
    Console().print(group)

horizon

Functions:

Name Description
build_horizon_profile_panel
build_horizon_profile_table
generate_horizon_profile_polar_plot
build_horizon_profile_panel
build_horizon_profile_panel(horizon_profile_table) -> Panel
Source code in pvgisprototype/cli/print/performance/horizon.py
def build_horizon_profile_panel(horizon_profile_table) -> Panel:
    """ """
    return Panel(
        horizon_profile_table,
        # subtitle="Horizon height profile",
        # subtitle_align="right",
        box=MINIMAL,
        # safe_box=True,
        # border_style=None,
        # style="",
        expand=False,
        padding=0,
        width=60,
    )
build_horizon_profile_table
build_horizon_profile_table() -> Table
Source code in pvgisprototype/cli/print/performance/horizon.py
def build_horizon_profile_table() -> Table:
    """ """
    horizon_profile_table = Table(
        box=None,
        show_header=True,
        header_style="bold dim",
        show_edge=False,
        pad_edge=False,
    )
    horizon_profile_table.add_column(
        f"{HORIZON_HEIGHT_NAME}",
        # justify="right",
        style="bold", no_wrap=True
    )

    return horizon_profile_table
generate_horizon_profile_polar_plot
generate_horizon_profile_polar_plot(
    horizon_profile,
) -> Plot
Source code in pvgisprototype/cli/print/performance/horizon.py
def generate_horizon_profile_polar_plot(
        horizon_profile,
        ) -> Plot:
    """
    """
    azimuthal_directions_radians = linspace(0, 2 * pi, horizon_profile.size)
    from pvgisprototype.cli.plot.uniplot import Plot
    horizon_profile_polar_plot = Plot(
        xs=degrees(azimuthal_directions_radians),
        ys=horizon_profile,
        lines=True,
        width=45,
        height=3,
        x_gridlines=[],
        y_gridlines=[],
        character_set="braille",
        # color=[colors[1]],
        # legend_labels=[labels[1]],
        color=["blue"],  # Add color
        legend_labels=["Horizon Profile"],  # Add legend
        interactive=False,
    )

    return horizon_profile_polar_plot

metadata

Functions:

Name Description
build_algorithmic_metadata_panel
build_algorithmic_metadata_table
populate_algorithmic_metadata_table
build_algorithmic_metadata_panel
build_algorithmic_metadata_panel(
    algorithmic_metadata_table,
) -> Panel | None
Source code in pvgisprototype/cli/print/performance/metadata.py
def build_algorithmic_metadata_panel(algorithmic_metadata_table) -> Panel | None:
    """ """
    if algorithmic_metadata_table is None:
        return None

    else:
        return Panel(
            algorithmic_metadata_table,
            subtitle="Algorithmic metadata",
            subtitle_align="right",
            # box=None,
            safe_box=True,
            style="",
            expand=False,
            padding=(0, 3),
        )
build_algorithmic_metadata_table
build_algorithmic_metadata_table() -> Table
Source code in pvgisprototype/cli/print/performance/metadata.py
def build_algorithmic_metadata_table() -> Table:
    """ """
    algorithmic_metadata_table = Table(
        box=None,
        show_header=True,
        header_style="bold dim",
        show_edge=False,
        pad_edge=False,
    )
    algorithmic_metadata_table.add_column(
        f"{TIMING_ALGORITHM_NAME}", justify="center", style="bold", no_wrap=True
    )
    algorithmic_metadata_table.add_column(
        f"{POSITIONING_ALGORITHM_NAME}", justify="center", style="bold", no_wrap=True
    )
    # algorithmic_metadata_table.add_column(
    #     # f"{INCIDENCE_ALGORITHM_NAME}", justify="center", style="bold", no_wrap=True
    #     f"{INCIDENCE_NAME}", justify="center", style="bold", no_wrap=True
    # )
    algorithmic_metadata_table.add_column(
        f"{AZIMUTH_ORIGIN_NAME}", justify="center", style="bold", no_wrap=True
    )
    algorithmic_metadata_table.add_column(
        f"{INCIDENCE_DEFINITION}", justify="center", style="bold", no_wrap=True
    )
    algorithmic_metadata_table.add_column(
        f"{SHADING_ALGORITHM_NAME}", justify="center", style="bold", no_wrap=True
    )
    # algorithmic_metadata_table.add_column(
    #     f"{HORIZON_HEIGHT_NAME}", justify="center", style="bold", no_wrap=True
    # )

    return algorithmic_metadata_table
populate_algorithmic_metadata_table
populate_algorithmic_metadata_table(
    data_model: PhotovoltaicPower,
) -> Table | None
Source code in pvgisprototype/cli/print/performance/metadata.py
def populate_algorithmic_metadata_table(
        data_model: PhotovoltaicPower,
        ) -> Table | None:
    """
    """
    timing_algorithm = data_model.solar_timing_algorithm
    positioning_algorithm = data_model.solar_positioning_algorithm
    azimuth_origin = data_model.solar_azimuth.origin
    incidence_angle_definition = data_model.solar_incidence_definition
    incidence_algorithm = data_model.solar_incidence_model
    shading_algorithm = data_model.shading_algorithm
    if all(
        [
        timing_algorithm,
        positioning_algorithm,
        azimuth_origin,
        incidence_angle_definition,
        incidence_algorithm,
        ]
    ):
        algorithmic_metadata_table = build_algorithmic_metadata_table()
        algorithmic_metadata_table.add_row(
            f"{timing_algorithm}",
            f"{positioning_algorithm}",
            # f"{incidence_algorithm}",
            f"{azimuth_origin}",
            f"{incidence_angle_definition}, {incidence_algorithm}",
            f"{shading_algorithm}",
        )
        return algorithmic_metadata_table

    else:
        return None

photovoltaic_module

Functions:

Name Description
build_photovoltaic_module_panel
build_photovoltaic_module_table
populate_photovoltaic_module_table
build_photovoltaic_module_panel
build_photovoltaic_module_panel(
    photovoltaic_module_table,
) -> Panel
Source code in pvgisprototype/cli/print/performance/photovoltaic_module.py
def build_photovoltaic_module_panel(photovoltaic_module_table) -> Panel:
    """ """
    photovoltaic_module_panel = Panel(
        photovoltaic_module_table,
        subtitle="PV Module",
        subtitle_align="right",
        safe_box=True,
        expand=True,
        padding=(0, 2),
    )

    return photovoltaic_module_panel
build_photovoltaic_module_table
build_photovoltaic_module_table() -> Table
Source code in pvgisprototype/cli/print/performance/photovoltaic_module.py
def build_photovoltaic_module_table() -> Table:
    """ """
    photovoltaic_module_table = Table(
        box=None,
        show_header=True,
        header_style=None,
        show_edge=False,
        pad_edge=False,
    )
    photovoltaic_module_table.add_column("Tech", justify="right", style="bold")
    photovoltaic_module_table.add_column("Peak-Power", justify="center", style="bold")
    photovoltaic_module_table.add_column("Mount Type", justify="left", style="bold")

    return photovoltaic_module_table
populate_photovoltaic_module_table
populate_photovoltaic_module_table(
    table: Table, photovoltaic_power: PhotovoltaicPower
) -> Table
Source code in pvgisprototype/cli/print/performance/photovoltaic_module.py
def populate_photovoltaic_module_table(
    table: Table,
    photovoltaic_power: PhotovoltaicPower,
) -> Table:
    """ """
    photovoltaic_module, mount_type = photovoltaic_power.technology.split(":")
    # peak_power_unit = photovoltaic_power.presentation.peak_power_unit
    peak_power_unit = "Peak Power Unit"
    table.add_row(
        photovoltaic_module,
        f"[green]{photovoltaic_power.peak_power}[/green] {peak_power_unit}",
        mount_type,
    )

    return table

position

Functions:

Name Description
build_position_panel
build_position_table
populate_position_table

Populate the 'position' table for a photovoltaic module using attributes

build_position_panel
build_position_panel(position_table, width) -> Panel
Source code in pvgisprototype/cli/print/performance/position.py
def build_position_panel(position_table, width) -> Panel:
    """ """
    return Panel(
        position_table,
        # title="Positioning",  # Add title to provide context without being too bold
        # title_align="left",  # Align the title to the left
        subtitle="Solar Surface",
        subtitle_align="right",
        # box=None,
        safe_box=True,
        style="",
        border_style="dim",  # Soften the panel with a dim border style
        # expand=False,
        # expand=True,
        padding=(0, 2),
        width=width,
    )
build_position_table
build_position_table() -> Table
Source code in pvgisprototype/cli/print/performance/position.py
def build_position_table() -> Table:
    """ """
    position_table = Table(
        box=None,
        show_header=True,
        header_style="bold dim",
        show_edge=False,
        pad_edge=False,
    )
    position_table.add_column(
        f"{LATITUDE_NAME}", justify="center", style="bold", no_wrap=True
    )
    position_table.add_column(
        f"{LONGITUDE_NAME}", justify="center", style="bold", no_wrap=True
    )
    position_table.add_column(
        f"{ELEVATION_NAME}", justify="center", style="bold", no_wrap=True
    )
    position_table.add_column(
        f"{ORIENTATION_NAME}", justify="center", style="bold", no_wrap=True
    )
    position_table.add_column(
        f"{TILT_NAME}", justify="center", style="bold", no_wrap=True
    )
    position_table.add_column(
        f"{UNIT_NAME}", justify="center", style="dim", no_wrap=True
    )

    return position_table
populate_position_table
populate_position_table(
    table: Table,
    data_model: PhotovoltaicPower,
    latitude: float,
    longitude: float,
    elevation: float,
    surface_orientation: bool | float = True,
    surface_tilt: bool | float = True,
    rounding_places: int = 3,
) -> Table

Populate the 'position' table for a photovoltaic module using attributes from the input data model which must contain the positional input parameters of the function described under Parameters :

Returns:

Name Type Description
table Table
Source code in pvgisprototype/cli/print/performance/position.py
def populate_position_table(
    table: Table,
    data_model: PhotovoltaicPower,  # can this become a generic data model ?
    latitude: float,
    longitude: float,
    elevation: float,
    surface_orientation: bool | float = True,
    surface_tilt: bool | float = True,
    rounding_places: int = 3,
) -> Table:
    """
    Populate the 'position' table for a photovoltaic module using attributes
    from the input data model which must contain the _positional_ input
    parameters of the function described under Parameters :

    Parameters
    ----------
    - latitude
    - longitude
    - elevation
    - surface_orientation
    - surface_tilt

    Returns
    -------
    table: Table

    """
    latitude = round_float_values(
        latitude, rounding_places
    )  # rounding_places)
    # position_table.add_row(f"{LATITUDE_NAME}", f"[bold]{latitude}[/bold]")

    longitude = round_float_values(
        longitude, rounding_places
    )  # rounding_places)

    # surface_orientation: float | None = (
    #     dictionary.get(SURFACE_ORIENTATION_COLUMN_NAME, None)
    #     if surface_orientation
    #     else None
    # )
    # surface_orientation: float = round_float_values(
    #     surface_orientation, rounding_places
    # )
    # surface_orientation: float | None = dictionary.get(SURFACE_ORIENTATION_COLUMN_NAME)
    if surface_orientation:
        surface_orientation = data_model.surface_orientation
    if surface_orientation is not None:
        surface_orientation = round_float_values(
            surface_orientation, rounding_places
        )

    # Get and round surface_tilt if it's not None
    # surface_tilt: float | None = (
    #     dictionary.get(SURFACE_TILT_COLUMN_NAME, None) if surface_tilt else None
    # )
    # surface_tilt: float = round_float_values(surface_tilt, rounding_places)
    # surface_tilt: float | None = dictionary.get(SURFACE_TILT_COLUMN_NAME)
    if surface_tilt:
        surface_tilt = data_model.surface_tilt
    if surface_tilt is not None:
        surface_tilt = round_float_values(surface_tilt, rounding_places)

    table.add_row(
        f"{latitude}",
        f"{longitude}",
        f"{elevation}",
        f"{surface_orientation}",
        f"{surface_tilt}",
        f"{data_model.solar_incidence.unit}",
    )
    # position_table.add_row("Time :", f"{timestamp[0]}")
    # position_table.add_row("Time zone :", f"{timezone}")

    longest_label_length = max(len(key) for key in data_model.to_dictionary().keys())
    surface_position_keys = {
        SURFACE_ORIENTATION_NAME,
        SURFACE_TILT_NAME,
        # ANGLE_UNIT_NAME,
        # INCIDENCE_DEFINITION,
        # UNIT_NAME,
    }
    for key, value in data_model.output.items():
        if key in surface_position_keys:
            padded_key = f"{key} :".ljust(longest_label_length + 3, " ")
            if key == INCIDENCE_DEFINITION:
                value = f"[yellow]{value}[/yellow]"
            table.add_row(padded_key, str(value))

    return table

table

Functions:

Name Description
add_table_row

Adds a row to a table with automatic unit handling and optional percentage.

build_performance_table

Setup the main performance table with appropriate columns.

add_table_row
add_table_row(
    table,
    quantity,
    value,
    unit,
    mean_value,
    mean_value_unit,
    standard_deviation=None,
    percentage=None,
    reference_quantity=None,
    series: ndarray = array([]),
    timestamps: DatetimeIndex | Timestamp = now(),
    frequency: str = "YE",
    source: str | None = None,
    quantity_style=None,
    value_style: str = "cyan",
    unit_style: str = "cyan",
    mean_value_style: str = "cyan",
    mean_value_unit_style: str = "cyan",
    percentage_style: str = "dim",
    reference_quantity_style: str = "white",
    rounding_places: int = 1,
)

Adds a row to a table with automatic unit handling and optional percentage.

Parameters:

Name Type Description Default
table
    The table object to which the row will be added.
required
quantity
    The name of the quantity being added.
required
value
    The numerical value associated with the quantity.
required
base_unit
    The base unit of measurement for the value.
required
percentage
    Optional; the percentage change or related metric.
None
reference_quantity
    Optional; the reference quantity for the percentage.
None
rounding_places int
    Optional; the number of decimal places to round the value.
1
Notes
  • Round value if rounding_places specified.
  • Convert units from base_unit to a larger unit if value exceeds 1000.
  • Add row to specified table.
Source code in pvgisprototype/cli/print/performance/table.py
def add_table_row(
    table,
    quantity,
    value,
    unit,
    mean_value,
    mean_value_unit,
    standard_deviation = None,
    percentage = None,
    reference_quantity = None,
    series: numpy.ndarray = numpy.array([]),
    timestamps: DatetimeIndex | Timestamp = Timestamp.now(),
    frequency: str = "YE",
    source: str | None = None,
    quantity_style = None,
    value_style: str = "cyan",
    unit_style: str = "cyan",
    mean_value_style: str = "cyan",
    mean_value_unit_style: str = "cyan",
    percentage_style: str = "dim",
    reference_quantity_style: str = "white",
    rounding_places: int = 1,
):
    """
    Adds a row to a table with automatic unit handling and optional percentage.

    Parameters
    ----------
    table :
                The table object to which the row will be added.
    quantity :
                The name of the quantity being added.
    value :
                The numerical value associated with the quantity.
    base_unit :
                The base unit of measurement for the value.
    percentage :
                Optional; the percentage change or related metric.
    reference_quantity :
                Optional; the reference quantity for the percentage.
    rounding_places :
                Optional; the number of decimal places to round the value.

    Notes
    -----
    - Round value if rounding_places specified.
    - Convert units from base_unit to a larger unit if value exceeds 1000.
    - Add row to specified table.

    """
    effects = {
        REFLECTIVITY,
        SPECTRAL_EFFECT_NAME,
        TEMPERATURE_AND_LOW_IRRADIANCE_COLUMN_NAME,
        SYSTEM_LOSS,
        NET_EFFECT,
    }

    if value is None or numpy.isnan(value):
        signed_value = "-"  # this _is_ the variable added in a row !
    else:
        if isinstance(value, (float, numpy.float32, numpy.float64, int, numpy.int32, numpy.int64)):
            styled_value = (
                f"[{value_style}]{value:.{rounding_places}f}"
                if value_style
                else f"{value:.{rounding_places}f}"
            )
            signed_value = (
                f"[{quantity_style}]+{styled_value}"
                if quantity in effects and value > 0
                else styled_value
            )
        else:
            raise TypeError(f"Unexpected type for value: {type(value)}")

    # Need first the unstyled quantity for the `signed_value` :-)
    quantity = f"[{quantity_style}]{quantity}" if quantity_style else quantity

    # Mean value and unit
    mean_value = (
        f"[{mean_value_style}]{mean_value:.{rounding_places}f}"
        if mean_value_style
        else f"{mean_value:.{rounding_places}f}"
    )
    if standard_deviation:
        standard_deviation = (
            f"[{mean_value_style}]{standard_deviation:.{rounding_places}f}"
            if mean_value_style
            else f"{standard_deviation:.{rounding_places}f }"
        )
    else:
        standard_deviation = ""

    # Style the unit
    unit = f"[{unit_style}]{unit}" if unit_style else unit

    # Get the reference quantity
    reference_quantity = (
        f"[{reference_quantity_style}]{reference_quantity}"
        if reference_quantity_style
        else reference_quantity
    )

    # Build the sparkline
    sparkline = (
        convert_series_to_sparkline(series, timestamps, frequency)
        if series.size > 0
        else ""
    )

    # Prepare the basic row data structure
    row = [quantity, signed_value, unit]

    # Add percentage and reference quantity if applicable
    if percentage is not None:
        # percentage = f"[red]{percentage:.{rounding_places}f}" if percentage < 0 else f"[{percentage_style}]{percentage:.{rounding_places}f}"
        percentage = (
            f"[red bold]{percentage:.{rounding_places}f}"
            if percentage < 0
            else f"[green bold]+{percentage:.{rounding_places}f}"
        )
        row.extend([f"{percentage}"])
        if reference_quantity:
            row.extend([reference_quantity])
        else:
            row.extend([""])
    else:
        row.extend(["", ""])
    if sparkline:
        row.extend([sparkline])
    if mean_value:
        if not sparkline:
            row.extend([""])
        row.extend([mean_value, mean_value_unit, (standard_deviation)])
    else:
        row.extend([""])
    if source:
        row.extend([source])

    # table.add_row(
    #     quantity,
    #     value,
    #     unit,
    #     percentage,
    #     reference_quantity,
    #     style=quantity_style
    # )
    table.add_row(*row)
build_performance_table
build_performance_table(
    frequency_label: str,
    quantity_style: str,
    value_style: str,
    unit_style: str,
    mean_value_unit_style: str,
    percentage_style: str,
) -> Table

Setup the main performance table with appropriate columns.

Source code in pvgisprototype/cli/print/performance/table.py
def build_performance_table(
    frequency_label: str,
    quantity_style: str,
    value_style: str,
    unit_style: str,
    mean_value_unit_style: str,
    percentage_style: str,
    # reference_quantity_style,
) -> Table:
    """
    Setup the main performance table with appropriate columns.
    """
    table = Table(
        # title="Photovoltaic Performance",
        # caption="Detailed view of changes in photovoltaic performance.",
        show_header=True,
        header_style="bold magenta",
        # show_footer=True,
        # row_styles=["none", "dim"],
        box=SIMPLE_HEAD,
        highlight=True,
    )
    table.add_column(
        "Quantity",
        justify="left",
        style=quantity_style,  # style="magenta",
        no_wrap=True,
    )
    table.add_column(
        "Total",  # f"{SYMBOL_SUMMATION}",
        justify="right",
        style=value_style,  # style="cyan",
    )
    table.add_column(
        "Unit",
        justify="left",
        style=unit_style,  # style="magenta",
    )
    table.add_column(
        "%",
        justify="right",
        style=percentage_style,  # style="dim",
    )
    table.add_column(
        "of",
        justify="left",
        style="dim",  # style=reference_quantity_style)
    )
    table.add_column(f"{frequency_label} Sums", style="dim", justify="center")
    # table.add_column(f"{frequency_label} Mean", justify="right", style="white dim")#style=value_style)
    table.add_column("Mean", justify="right", style="white dim")  # style=value_style)
    table.add_column(
        "Unit",  # for Mean values
        justify="left",
        style=mean_value_unit_style,
    )

    table.add_column(
        "Variability", justify="right", style="dim"
    )  # New column for standard deviation
    table.add_column("Source", style="dim", justify="left")

    return table

position

Modules:

Name Description
caption
data
table

caption

Functions:

Name Description
build_solar_position_model_caption
build_solar_position_model_caption
build_solar_position_model_caption(
    solar_position_model_data,
    caption,
    timezone,
    user_requested_timezone,
    surface_orientation=True,
    surface_tilt=True,
)
Source code in pvgisprototype/cli/print/position/caption.py
def build_solar_position_model_caption(
    solar_position_model_data,
    caption,
    timezone,
    user_requested_timezone,
    surface_orientation=True,
    surface_tilt=True,
):
    """
    """
    # Definitions
    ## Azimuth origin : North
    azimuth_origin = solar_position_model_data.get(SolarPositionParameterMetadataColumnName.azimuth_origin, None)
    ## Timezone or UTC

    ## Positions sun-to-horizon : from the set {['Above', 'Low angle', 'Below']}
    ## for which calculations were performed !
    if solar_position_model_data.get(SolarPositionParameterMetadataColumnName.sun_horizon_positions, None):
        sun_horizon_positions = [position.value for position in solar_position_model_data.get(SolarPositionParameterMetadataColumnName.sun_horizon_positions, None)]
    else:
        sun_horizon_positions = None

    # Algorithms
    ## Timing : e.g. NOAA
    timing_algorithm = solar_position_model_data.get(SolarPositionParameterColumnName.timing, None)

    ## Positioning : e.g. NOAA
    # solar_positioning_algorithm = get_value_or_default(
    solar_positioning_algorithm = solar_position_model_data.get(
        # solar_position_model, POSITIONING_ALGORITHM_NAME, None
        SolarPositionParameterColumnName.positioning, None
    )
    adjusted_for_atmospheric_refraction = solar_position_model_data.get('Unrefracted ⌮', None)

    ## Incidence : e.g. Iqbal
    incidence_algorithm = solar_position_model_data.get(
            # solar_position_model, INCIDENCE_ALGORITHM_NAME, None
            SolarPositionParameterMetadataColumnName.incidence_algorithm, None
        )
    ## Incidence angle : e.g. Sun-Vector-to-Surface-Plane
    solar_incidence_definition = None
    if incidence_algorithm:
        solar_incidence_definition = solar_position_model_data.get(SolarPositionParameterMetadataColumnName.incidence_definition, None)

    ## Shading : e.g. PVGIS
    shading_algorithm = get_value_or_default(
        solar_position_model_data, SolarPositionParameterMetadataColumnName.shading_algorithm, None
    )

    ## Shading states : ['all']  -- look corresponding Enum class

    # Constants for earth's orbit eccentricity
    ## Eccentricity Offset : 0.048869
    eccentricity_phase_offset = solar_position_model_data.get(
        SolarPositionParameterMetadataColumnName.eccentricity_phase_offset, None
    )
    ## Eccentricity Amplitude ⋅⬭ : 0.03344
    eccentricity_amplitude = solar_position_model_data.get(
        SolarPositionParameterMetadataColumnName.eccentricity_amplitude, None
    )

    # if solar_position_model.get(SHADING_STATES_COLUMN_NAME) is not None:
    #     shading_states = [state.value for state in solar_position_model.get(SHADING_STATES_COLUMN_NAME, None)]
    # else:
    #     shading_states = None

    # ----------------------------------------------------------------
    if surface_orientation or surface_tilt:
        caption += "\n[underline]Definitions[/underline]  "

    if azimuth_origin:
        caption += (
            f"Azimuth origin : [bold green]{azimuth_origin}[/bold green], "
        )

    # Fundamental Definitions

    if solar_incidence_definition is not None:
        caption += f"{SolarPositionParameterMetadataColumnName.incidence_angle.value}: [bold yellow]{solar_incidence_definition}[/bold yellow] "

    if sun_horizon_positions:
        caption += f"Sun-to-Horizon: [bold]{sun_horizon_positions}[/bold]"

    # Algorithms

    if timing_algorithm or solar_positioning_algorithm:
        caption += "\n[underline]Algorithms[/underline]  "

    if timing_algorithm:
        caption += f"Timing: [bold]{timing_algorithm}[/bold], "

    ## Timezone is part of the `time_panel`

    if solar_positioning_algorithm:
        caption += f"Positioning: [bold]{solar_positioning_algorithm}[/bold], "

    if adjusted_for_atmospheric_refraction:
        # caption += f"\n[underline]Atmospheric Properties[/underline]  "
        caption += f"Unrefracted zenith: [bold]{adjusted_for_atmospheric_refraction}[/bold], "

    if incidence_algorithm:
        caption += f"Incidence: [bold yellow]{incidence_algorithm}[/bold yellow], "

    if shading_algorithm:
        caption += f"Shading: [bold]{shading_algorithm}[/bold]"

    # if shading_states: # Not implemented for the position commands !
    #     caption += f"Shading states : [bold]{shading_states}[/bold]"

    if any([eccentricity_phase_offset, eccentricity_amplitude]):
        caption += "\n[underline]Constants[/underline] "
        if eccentricity_phase_offset and eccentricity_amplitude:
            caption += f"{SolarPositionParameterMetadataColumnName.eccentricity_phase_offset_short.value}: {eccentricity_phase_offset}, "
            caption += f"{SolarPositionParameterMetadataColumnName.eccentricity_amplitude.value}: {eccentricity_amplitude}, "

    return caption.rstrip(", ")  # Remove trailing comma + space

data

Functions:

Name Description
print_solar_position_series_in_columns
print_solar_position_series_table
print_solar_position_table_panels
print_solar_position_series_in_columns
print_solar_position_series_in_columns(
    longitude,
    latitude,
    timestamps,
    timezone,
    table,
    rounding_places=ROUNDING_PLACES_DEFAULT,
    index: bool = False,
)
Source code in pvgisprototype/cli/print/position/data.py
def print_solar_position_series_in_columns(
    longitude,
    latitude,
    timestamps,
    timezone,
    table,
    rounding_places=ROUNDING_PLACES_DEFAULT,
    index: bool = False,
):
    """ """
    panels = []

    # Iterating through each timestamp
    for i, timestamp in enumerate(timestamps):
        table_panel = Table(title=f"Time: {timestamp}", box=ROUNDED)
        table_panel.add_column("Parameter", justify="right")
        table_panel.add_column("Value", justify="left")

        # Optionally add an index column
        if index:
            table_panel.add_column("Index", style="dim")
            table_panel.add_row("Index", str(i))

        # Add longitude, latitude, and other non-time-varying parameters
        table_panel.add_row("Longitude", str(longitude))
        table_panel.add_row("Latitude", str(latitude))

        # For each parameter of interest, aggregate across models for this timestamp
        parameters = [
            "Declination",
            "Hour Angle",
            "Zenith",
            "Altitude",
            "Azimuth",
            "Incidence",
        ]
        for param in parameters:
            # Assume `table` is a dictionary of models, each containing a list of values for each parameter
            values = [
                round_float_values(model_result[param][i], rounding_places)
                for _, model_result in table.items()
                if param in model_result
            ]
            value_str = ", ".join(map(str, values))  # Combine values from all models
            table_panel.add_row(param, value_str)

        panel = Panel(table_panel, expand=True)
        panels.append(panel)

    console.print(Columns(panels))
print_solar_position_series_table
print_solar_position_series_table(
    longitude,
    latitude,
    timestamps,
    timezone,
    table,
    position_parameters: Sequence[
        SolarPositionParameter
    ] = all,
    title="Solar position overview",
    index: bool = False,
    version: bool = False,
    fingerprint: bool = False,
    surface_orientation=None,
    surface_tilt=None,
    incidence=None,
    user_requested_timestamps=None,
    user_requested_timezone=None,
    rounding_places=ROUNDING_PLACES_DEFAULT,
    group_models=False,
    panels=False,
) -> None
Source code in pvgisprototype/cli/print/position/data.py
def print_solar_position_series_table(
    longitude,
    latitude,
    timestamps,
    timezone,
    table,
    position_parameters: Sequence[SolarPositionParameter] = SolarPositionParameter.all,
    title="Solar position overview",
    index: bool = False,
    version: bool = False,
    fingerprint: bool = False,
    surface_orientation=None,
    surface_tilt=None,
    incidence=None,
    user_requested_timestamps=None,
    user_requested_timezone=None,
    rounding_places=ROUNDING_PLACES_DEFAULT,
    group_models=False,
    panels=False,
) -> None:
    """ """
    rounded_table = round_float_values(table, rounding_places)

    if panels:
        if timestamps.size == 1:
            print_solar_position_table_panels(
                longitude=longitude,
                latitude=latitude,
                timestamp=timestamps,
                timezone=timezone,
                solar_position_table=rounded_table,
                position_parameters=position_parameters,
                rounding_places=rounding_places,
                user_requested_timestamp=user_requested_timestamps,
                user_requested_timezone=user_requested_timezone,
            )
    else:
        longitude = round_float_values(longitude, rounding_places)
        latitude = round_float_values(latitude, rounding_places)

        # Build the main caption

        caption = build_caption(
            data_dictionary=rounded_table,
            longitude=longitude,
            latitude=latitude,
            # elevation=elevation,
            rounding_places=rounding_places,
            surface_orientation=True,
            surface_tilt=True,
        )

        # Iterate over multiple solar position models -- we _can_ have many !

        for _, model_result in rounded_table.items():
            if model_result:
                model_result = flatten_dictionary(model_result)

                # Update the caption with model-specific metadata

                model_caption = build_solar_position_model_caption(
                    solar_position_model_data=model_result,
                    caption=caption,
                    timezone=timezone,
                    user_requested_timezone=user_requested_timezone,
                )

                # then : Create a Legend table for the symbols in question

                legend = build_legend_table(
                    dictionary=model_result,
                    caption=model_caption,
                    show_header=False,
                    box=None,
                )

                # Time might be Local 

                if user_requested_timestamps is not None:
                    time_column_name = LOCAL_TIME_COLUMN_NAME
                else:
                    time_column_name = TIME_COLUMN_NAME

                if timestamps is not None:
                    if user_requested_timezone is not None:
                        if user_requested_timezone != ZoneInfo("UTC"):
                            time_column_name = LOCAL_TIME_COLUMN_NAME
                            timezone_string = f"Local Zone: [bold]{timezone}[/bold]"
                        else:
                            time_column_name = TIME_COLUMN_NAME

                if timezone:
                    if timezone == ZoneInfo('UTC'):
                        timezone_string = f"[bold]{timezone}[/bold]"
                    else:
                        timezone_string = f"Local Zone: [bold]{timezone}[/bold]"

                # Build solar position table structure

                solar_position_table = build_solar_position_table(
                    title=title,
                    index=index,
                    input_table=rounded_table,
                    dictionary=model_result,
                    position_parameters=position_parameters,
                    # timestamps=timestamps,
                    # rounding_places=rounding_places,
                    time_column_name=time_column_name,
                    time_column_footer=f"{SYMBOL_SUMMATION} / [blue]{SYMBOL_MEAN}[/blue]",  # Abusing this "cell" as a "Row Name"
                    time_column_footer_style="purple",  # to make it somehow distinct from the Column !
                    # keys_to_sum = KEYS_TO_SUM,
                    # keys_to_average = KEYS_TO_AVERAGE,
                    # keys_to_exclude = KEYS_TO_EXCLUDE,
                )

                # -------------------------------------------------- Ugly Hack ---
                if user_requested_timestamps is not None:
                    timestamps = user_requested_timestamps
                # --- Ugly Hack --------------------------------------------------

                output_table = populate_solar_position_table(
                    table=solar_position_table,
                    model_result=model_result,
                    timestamps=timestamps,
                    index=index,
                    rounding_places=rounding_places,
                )

                # Create Panels for time, caption and legend

                time_table = build_time_table()
                frequency, frequency_label = infer_frequency_from_timestamps(timestamps)
                time_table.add_row(
                    str(timestamps.strftime("%Y-%m-%d %H:%M").values[0]),
                    str(frequency) if frequency and frequency != "Single" else "-",
                    str(timestamps.strftime("%Y-%m-%d %H:%M").values[-1]),
                    str(timezone_string),
                )
                time_panel = build_time_panel(time_table, padding=(0, 1, 2, 1))

                # Version & Fingerprint

                fingerprint = retrieve_fingerprint(dictionary=model_result)
                version_and_fingerprint_and_column = (
                    build_version_and_fingerprint_columns(
                        version=version,
                        fingerprint=fingerprint,
                    )
                )

                # if verbose:  # Print if requested via at least 1x `-v` ?
                print_solar_position_table_and_metadata_panels(
                    time=time_panel,
                    caption=model_caption,
                    table=output_table,
                    legend=legend,
                    fingerprint=version_and_fingerprint_and_column,
                )
print_solar_position_table_panels
print_solar_position_table_panels(
    longitude,
    latitude,
    timestamp,
    timezone,
    solar_position_table,
    rounding_places=ROUNDING_PLACES_DEFAULT,
    position_parameters=all,
    surface_orientation=True,
    surface_tilt=True,
    user_requested_timestamp=None,
    user_requested_timezone=None,
) -> None
Source code in pvgisprototype/cli/print/position/data.py
def print_solar_position_table_panels(
    longitude,
    latitude,
    timestamp,
    timezone,
    solar_position_table,
    rounding_places=ROUNDING_PLACES_DEFAULT,
    position_parameters=SolarPositionParameter.all,
    surface_orientation=True,
    surface_tilt=True,
    user_requested_timestamp=None,
    user_requested_timezone=None,
) -> None:
    """
    """
    first_model = solar_position_table[next(iter(solar_position_table))]
    panels = []

    # surface position Panel
    table = Table(box=None, show_header=False, show_edge=False, pad_edge=False)
    table.add_column(justify="right", style="none", no_wrap=True)
    table.add_column(justify="left")
    table.add_row(f"{LATITUDE_COLUMN_NAME} :", f"[bold]{latitude}[/bold]")
    table.add_row(f"{LONGITUDE_COLUMN_NAME} :", f"[bold]{longitude}[/bold]")
    table.add_row("Time :", f"{timestamp[0]}")
    table.add_row("Time zone :", f"{timezone}")
    longest_label_length = max(len(key) for key in first_model.keys())
    surface_position_keys = {
        SURFACE_ORIENTATION_NAME,
        SURFACE_TILT_NAME,
        ANGLE_UNIT_NAME,
        INCIDENCE_DEFINITION,
        UNIT_NAME,
    }
    for key, value in first_model.items():
        if key in surface_position_keys:
            padded_key = f"{key} :".ljust(longest_label_length + 3, " ")
            if key == INCIDENCE_DEFINITION:
                value = f"[yellow]{value}[/yellow]"
            table.add_row(padded_key, str(value))
    position_panel = Panel(
        table,
        title="Surface Position",
        box=HORIZONTALS,
        style="",
        expand=False,
        padding=(0, 2),
    )
    panels.append(position_panel)

    # solar position Panel/s
    for model_result in solar_position_table.values():
        table = Table(box=None, show_header=False, show_edge=False, pad_edge=False)
        table.add_column(justify="right", style="none", no_wrap=True)
        table.add_column(justify="left")

        longest_label_length = max(len(key) for key in model_result.keys())
        _index = 0

        position_parameter_values = {
            SolarPositionParameter.declination: lambda idx=_index: get_scalar(
                get_value_or_default(model_result, DECLINATION_COLUMN_NAME),
                idx,
                rounding_places,
            ),
            # SolarPositionParameter.timing: lambda idx=_index: str(get_value_or_default(
            #     model_result, TIME_ALGORITHM_NAME
            # )),
            # SolarPositionParameter.positioning: lambda idx=_index: str(get_value_or_default(
            #     model_result, POSITIONING_ALGORITHM_NAME
            # )),
            SolarPositionParameter.hour_angle: lambda idx=_index: get_scalar(
                get_value_or_default(model_result, HOUR_ANGLE_COLUMN_NAME),
                idx,
                rounding_places,
            ),
            SolarPositionParameter.zenith: lambda idx=_index: get_scalar(
                get_value_or_default(model_result, ZENITH_COLUMN_NAME),
                idx,
                rounding_places,
            ),
            SolarPositionParameter.altitude: lambda idx=_index: get_scalar(
                get_value_or_default(model_result, ALTITUDE_COLUMN_NAME, None),
                idx,
                rounding_places,
            ),
            SolarPositionParameter.azimuth: lambda idx=_index: get_scalar(
                get_value_or_default(model_result, AZIMUTH_COLUMN_NAME),
                idx,
                rounding_places,
            ),
            SolarPositionParameter.incidence: lambda idx=_index: get_scalar(
                get_value_or_default(model_result, INCIDENCE_COLUMN_NAME),
                idx,
                rounding_places,
            ),
            SolarPositionParameter.event_time: lambda idx=_index: get_scalar(
                get_value_or_default(model_result, SOLAR_EVENT_TIME_COLUMN_NAME),
                idx,
                rounding_places,
            ),
            SolarPositionParameter.event_type: lambda idx=_index: get_scalar(
                get_value_or_default(model_result, SOLAR_EVENT_COLUMN_NAME),
                idx,
                rounding_places,
            ),
        }
        for parameter in position_parameters:
            if parameter in position_parameter_values:
                padded_key = f"{parameter.value} :".ljust(longest_label_length + 1, " ")
                value = position_parameter_values[parameter]()
                if parameter == AZIMUTH_ORIGIN_NAME:
                    value = f"[yellow]{value}[/yellow]"
                table.add_row(padded_key, str(value))

        title = f"[bold]{get_value_or_default(model_result, POSITIONING_ALGORITHM_NAME)}[/bold]"
        panel = Panel(
            table,
            title=title,
            box=ROUNDED,
            # style=panel_style,
            padding=(0, 2),
        )
        panels.append(panel)

    columns = Columns(panels, expand=True, equal=True, padding=2)
    console.print(columns)

table

Functions:

Name Description
build_solar_position_table

Notes

populate_solar_position_table

Populates a Rich Table with solar position data

print_solar_position_table_and_metadata_panels
build_solar_position_table
build_solar_position_table(
    title: str,
    index: bool,
    input_table: dict,
    dictionary: dict,
    position_parameters: Sequence[SolarPositionParameter],
    time_column_name: str,
    time_column_footer: RenderableType = SYMBOL_SUMMATION,
    time_column_footer_style: str = "purple",
) -> Table
Notes

The input dictionary is a flat structure !

Source code in pvgisprototype/cli/print/position/table.py
def build_solar_position_table(
    title: str,
    index: bool,
    input_table: dict,
    dictionary: dict,
    position_parameters: Sequence[SolarPositionParameter],
    # timestamps,
    # rounding_places: int,
    time_column_name: str,
    time_column_footer: RenderableType = SYMBOL_SUMMATION,
    time_column_footer_style: str = "purple",
    # keys_to_sum = KEYS_TO_SUM,
    # keys_to_average = KEYS_TO_AVERAGE,
    # keys_to_exclude = KEYS_TO_EXCLUDE,
) -> Table:
    """
    Notes
    -----

    The input `dictionary` is a flat structure !
    """
    from rich.table import Table
    from rich.box import SIMPLE_HEAD

    table = Table(
        title=title,
        caption_justify="left",
        expand=False,
        padding=(0, 1),
        box=SIMPLE_HEAD,
        show_footer=True,
        show_header=True,
        header_style="bold",  # "bold gray50",
        row_styles=["none", "dim"],
        highlight=True,  # To light or Not ?
    )

    # base columns
    if index:
        table.add_column("Index")

    ## Time column

    table.add_column(
        time_column_name,
        justify="left",
        no_wrap=True,
        footer=time_column_footer,
        footer_style=time_column_footer_style,
    )

    # remove the 'Title' entry! ---------------------------------------------
    dictionary.pop("Title", NOT_AVAILABLE)
    # ------------------------------------------------------------- Important

    # Get the data

    first_model = input_table[next(iter(input_table))]
    # `first_model` contains the "data" in case of a single `position` command
    # i.e. `position azimuth`

    # In case of the `position overview` command, however :
    # Pull out the two relevant nested dicts in case of the overview command !?
    core_data = first_model.get("Core", {})
    events_data = first_model.get("Solar Events", {})

    # For each requested parameter, derive its header and find its data
    for parameter in position_parameters:

        # Skip enum members without a matching ColumnName
        if parameter.name not in SolarPositionParameterColumnName.__members__ \
           or parameter.name in ("timing", "positioning"):
            continue

        # Get the human-readable header from the ColumnName enum
        header = SolarPositionParameterColumnName[parameter.name].value
        value = find_nested_value(first_model, header)
        if value is None:
            if header in core_data:
                value = find_nested_value(core_data, header)
            elif header in events_data:
                value = find_nested_value(events_data, header)
            else:
                continue  # not present

        from numpy import datetime64, isnat
        if parameter in (
            SolarPositionParameter.event_type,
            SolarPositionParameter.event_time,
        ):
            # If value is None, there is no event array at all—skip
            if value is None:
                continue

            # For event_time: dtype datetime64, for event_type: object dtype/SolarEvent
            # If value is not an array, make it a list so iteration succeeds
            if not hasattr(value, "__iter__") or isinstance(value, str):
                value_list = [value]
            else:
                value_list = value

            def is_real_event(ev):
                # For datetime columns (event_time)
                if isinstance(ev, datetime64):
                    return not isnat(ev)
                # For event_type columns
                if ev is not None and hasattr(ev, "name") and ev.name == "none":
                    return False
                return ev not in (None, "None")

            has_data = any(is_real_event(v) for v in value_list)
            if not has_data:
                continue  # skip entirely

        table.add_column(header)

    return table
populate_solar_position_table
populate_solar_position_table(
    table: Table,
    model_result: dict,
    timestamps,
    index: bool,
    rounding_places: int,
    sparkline: bool = False,
)

Populates a Rich Table with solar position data using the already-built table structure (columns). Compatible with flattened model_result dictionaries.

Source code in pvgisprototype/cli/print/position/table.py
def populate_solar_position_table(
    table: Table,
    model_result: dict,
    timestamps,
    index: bool,
    rounding_places: int,
    # position_parameters: Sequence[SolarPositionParameter],
    sparkline: bool = False,
):
    """
    Populates a Rich Table with solar position data
    using the already-built table structure (columns).
    Compatible with flattened model_result dictionaries.
    """
    from numpy import datetime64, bool_

    for idx, timestamp in enumerate(timestamps):
        row = []
        if index:
            row.append(str(idx + 1))
        row.append(to_datetime(timestamp).strftime("%Y-%m-%d %H:%M:%S"))

        # Iterate over each table column (already reflecting the final structure)
        for column in table.columns:
            header = column.header
            if header in ("Index", "Time"):
                continue

            # Retrieve matching array from model_result
            value_array = model_result.get(header)
            if value_array is None or len(value_array) <= idx:
                row.append("")
                continue

            value = get_scalar(value_array, idx, rounding_places)

            # Format final cell output
            if value is None:
                row.append("")

            elif isinstance(value, SolarEvent):
                row.append(format_string(value.value))

            elif isinstance(value, str):
                vl = value.lower()
                style = {
                    "above": "bold yellow",
                    "low angle": "dark_orange",
                    "below": "red",
                }.get(vl)
                row.append(Text(value, style=style) if style else value)

            elif isinstance(value, datetime64):
                if isna(value):
                    row.append("")
                else:
                    dt = value.astype("datetime64[s]").astype("O")
                    row.append(str(dt.time()))

            elif isinstance(value, bool_) or isinstance(value, bool):
                # bool _must_ come before numeric !
                row.append(str(bool(value)))

            elif isinstance(value, (int, float, numpy.generic)):
                rounded = str(round_float_values(value, rounding_places))

                style = "bold cyan"
                if value < 0:
                    style = "bold red"
                elif value == 0:
                    style = "highlight dim"
                row.append(Text(rounded, style=style))

            else:
                row.append(str(value))

        table.add_row(*row)

    if sparkline:
        # Build a footer row of sparklines (or blank) for each column
        footer_cells = []
        for column in table.columns:
            header = column.header

            # Keep index/time columns blank
            if header in ("Index", "Time"):
                # footer_cells.append("")
                footer_cells.append("Sparkline")
                continue

            # Retrieve the full series for this column
            series = model_result.get(header)
            if series is None or len(series) == 0:
                footer_cells.append("")
                continue

            # Only numeric series get sparklines
            # Skip booleans, strings, events, datetimes
            if not isinstance(series, (list, tuple)) and hasattr(series, "dtype"):
                dtype = series.dtype
            else:
                footer_cells.append("")
                continue

            if dtype.kind in ("i", "u", "f"):
                # Generate sparkline: pass the raw numpy array and timestamps
                from pvgisprototype.cli.print.sparklines import (
                    convert_series_to_sparkline,
                )

                spark = convert_series_to_sparkline(
                    series=series,
                    timestamps=timestamps,
                    frequency=timestamps.freq,
                )
                footer_cells.append(Text(spark, style="dim"))
            else:
                footer_cells.append("")

        # Add the sparkline footer as a final row
        table.add_row(*footer_cells)

    return table
print_solar_position_table_and_metadata_panels
print_solar_position_table_and_metadata_panels(
    time,
    caption,
    table,
    legend,
    fingerprint,
    subtitle_caption="Reference",
    subtitle_legend="Legend",
)
Source code in pvgisprototype/cli/print/position/table.py
def print_solar_position_table_and_metadata_panels(
    time,
    caption,
    table,
    legend,
    fingerprint,
    subtitle_caption="Reference",
    subtitle_legend="Legend",
):
    """ """
    console = Console()
    console.print(table)

    panels = []

    if time:
        panels.append(
            time,
        )

    if caption:
        panels.append(
            Panel(
                caption,
                subtitle=f"[gray]{subtitle_caption}[/gray]",
                subtitle_align="right",
                border_style="dim",
                expand=False,
            )
        )

    if legend:
        panels.append(
            Panel(
                legend,
                subtitle=f"[dim]{subtitle_legend}[/dim]",
                subtitle_align="right",
                border_style="dim",
                # style="dim",
                expand=False,
                padding=(0, 1),
            )
        )

    if panels:
        console.print(Columns(panels))

    if fingerprint:  # version & fingerprint
        console.print(fingerprint)

qr

Functions:

Name Description
print_quick_response_code

print_quick_response_code

print_quick_response_code(
    dictionary: dict,
    longitude: float,
    latitude: float,
    elevation: float | None = None,
    surface_orientation: bool = True,
    surface_tilt: bool = True,
    timestamps: DatetimeIndex = DatetimeIndex([now()]),
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    optimal_surface_position: bool = False,
    output_type: QuickResponseCode = Base64,
) -> None
Source code in pvgisprototype/cli/print/qr.py
def print_quick_response_code(
    dictionary: dict,
    longitude: float,
    latitude: float,
    elevation: float | None = None,
    surface_orientation: bool = True,
    surface_tilt: bool = True,
    timestamps: DatetimeIndex = DatetimeIndex([datetime.now()]),
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    optimal_surface_position: bool = False,
    output_type: QuickResponseCode = QuickResponseCode.Base64,
) -> None:
    """ """

    if optimal_surface_position:
        quick_response_code = generate_quick_response_code_optimal_surface_position(
            dictionary=dictionary,
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            surface_orientation=True,
            surface_tilt=True,
            timestamps=timestamps,
            rounding_places=ROUNDING_PLACES_DEFAULT,
            output_type=output_type,
        )
    else:
        quick_response_code = generate_quick_response_code(
            dictionary=dictionary,
            longitude=longitude,
            latitude=latitude,
            elevation=elevation,
            surface_orientation=True,
            surface_tilt=True,
            timestamps=timestamps,
            rounding_places=ROUNDING_PLACES_DEFAULT,
            output_type=output_type,
        )

    if output_type.value == QuickResponseCode.Base64:
        print(quick_response_code)

    if output_type.value == QuickResponseCode.Image:
        quick_response_code.print_ascii()

quantity

Functions:

Name Description
print_quantity_table

print_quantity_table

print_quantity_table(
    dictionary: dict = dict(),
    title: str = "Series",
    main_key: ndarray | None = None,
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
    verbose=1,
    index: bool = False,
) -> None
Source code in pvgisprototype/cli/print/quantity.py
def print_quantity_table(
    dictionary: dict = dict(),
    title: str = "Series",
    main_key: ndarray | None = None,
    rounding_places: int = ROUNDING_PLACES_DEFAULT,
    verbose=1,
    index: bool = False,
) -> None:
    """
    """

    # Hacky
    dictionary = dictionary.to_dictionary()  # custom data model method !
    table = Table(title=title, box=SIMPLE_HEAD)

    if index:
        table.add_column("Index")

    # remove the 'Title' entry! ---------------------------------------------
    # dictionary.pop(TITLE_KEY_NAME, NOT_AVAILABLE)
    # ------------------------------------------------------------- Important

    # # base columns
    # if verbose > 0:

    # additional columns based dictionary keys
    for key in dictionary.keys():
        if dictionary[key] is not None:
            table.add_column(key)

    if main_key is not None:  # consider the 1st key of having the "valid" number of values
        # main_key = list(dictionary.keys())[0]
        main_key = dictionary['value']  # Hacky, yet we know it !

    # Convert single float or int values to arrays of the same length as the "main' key
    for key, value in dictionary.items():
        if isinstance(value, (float, int)):
            dictionary[key] = full(main_key.shape, value)

        if isinstance(value, str):
            dictionary[key] = full(main_key.shape, str(value))

    # Zip series
    zipped_series = zip(*dictionary.values())

    # Populate table
    index_counter = 1
    for values in zipped_series:
        row = []

        if index:
            row.append(str(index_counter))
            index_counter += 1

        for idx, (column_name, value) in enumerate(zip(dictionary.keys(), values)):
            if idx == 0:  # assuming after 'Time' is the value of main interest
                bold_value = Text(
                    str(round_float_values(value, rounding_places)), style="bold"
                )
                row.append(bold_value)
            else:
                if not isinstance(value, str):
                    if SYMBOL_LOSS in column_name:
                        red_value = Text(
                            str(round_float_values(value, rounding_places)),
                            style="bold red",
                        )
                        row.append(red_value)
                    else:
                        row.append(str(round_float_values(value, rounding_places)))
                else:
                    row.append(value)
        table.add_row(*row)

    if verbose:
        Console().print(table)

series

Functions:

Name Description
print_series_statistics

print_series_statistics

print_series_statistics(
    data_array: ndarray,
    timestamps: DatetimeIndex,
    title: str = "Time series",
    groupby: str | None = None,
    monthly_overview: bool = False,
    rounding_places: int | None = None,
    verbose=VERBOSE_LEVEL_DEFAULT,
) -> None
Source code in pvgisprototype/cli/print/series.py
def print_series_statistics(
    data_array: numpy.ndarray,
    timestamps: DatetimeIndex,
    title: str = "Time series",
    groupby: str | None = None,
    monthly_overview: bool = False,
    rounding_places: int | None = None,
    verbose = VERBOSE_LEVEL_DEFAULT,
) -> None:
    """
    """
    rename_monthly_output_rows = {
        "Sum of Group Means": "Yearly PV energy",
        "Sum of Global Inclined Irradiance": "Yearly in-plane irradiance",
    }
    if groupby and groupby.lower() == "monthly":
        monthly_overview = True

    table = Table(
        title=title,
        caption="Caption text",
        show_header=True,
        header_style="bold magenta",
        row_styles=["none", "dim"],
        box=SIMPLE_HEAD,
        highlight=True,
    )

    # Compute statistics
    if monthly_overview:  # typical overview !
        table.add_column(
            "Statistic", justify="right", style="bright_blue", no_wrap=True
        )
        table.add_column("Value", style="cyan")
        # get monthly mean values
        statistics = calculate_series_statistics(data_array, timestamps, "M")
    else:
        table.add_column("Statistic", justify="right", style="magenta", no_wrap=True)
        table.add_column("Value", style="cyan")
        statistics = calculate_series_statistics(data_array, timestamps, groupby)

    # Basic Metadata
    basic_metadata = ["Start", "End", "Count"] if not monthly_overview else ["Start", "End"]
    for key in basic_metadata:
        if key in statistics:
            add_statistic_row(table, key, statistics[key])

    table.add_row("", "")  # --%<--- Separate ----

    # grouping_labels = {
    #     'Y': 'Yearly means',
    #     'S': 'Seasonal means',
    #     'M': 'Monthly means',
    #     'W': 'Weekly means',
    #     'D': 'Daily means',
    #     'H': 'Hourly means',
    # }

    # Groups by
    time_groupings = [
        "Yearly means",
        "Seasonal means",
        "Monthly means",
        "Weekly means",
        "Daily means",
        "Hourly means",
        f"{groupby} means" if groupby else None,  # do not print custom frequency means
    ]

    if monthly_overview:
        monthly_metadata = [
            # 'Min',
            # 'Mean',
            # 'Max',
            # 'Standard deviation',
            "Sum of Group Means",
            "Irradiance",
            f"Sum of Global Inclined Irradiance",
        ]

    # Add statistics
    for key, value in statistics.items():
        if not monthly_overview:
            if (
                key not in basic_metadata
                and key not in time_groupings
            ):
                add_statistic_row(table, key, value, rounding_places)
        else:
            if (
                key in monthly_metadata
                and key not in basic_metadata
            ):
                display_key = rename_monthly_output_rows.get(key, key)
                add_statistic_row(table, display_key, value, rounding_places)

    table.add_row("", "")  # --%<--- Separate --------------------------------

    # Process Groups with Helper
    custom_frequency_label = (
        f"{groupby} means" if groupby and groupby not in time_groupings else None
    )
    for group in time_groupings:
        if group in statistics:
            group_rows = format_group_statistics(statistics, group)
            for name, value in group_rows:
                add_statistic_row(table, name, value)

    # Custom frequency group
    if custom_frequency_label and custom_frequency_label in statistics:
        custom_freq_data = statistics[custom_frequency_label]
        # ----------------------------------------------------- the Old way --
        # period_count = 1
        # for value in custom_freq_data:
        #     label = f"{groupby} Period {period_count}"
        #     table.add_row(label, str(value))
        #     period_count += 1
        # table.add_row("", "")
        # ----------------------------------------------------- the Old way --
        for period_count, value in enumerate(custom_freq_data, start=1):
            add_statistic_row(
                table=table,
                key=f"{groupby} Period {period_count}",
                value=value,
                rounding_places=rounding_places,
            )

    # Index of items
    index_metadata = (
        [
            "Time of Min",
            "Index of Min",
            "Time of Max",
            "Index of Max",
        ]
        if not monthly_overview
        else []
    )
    # # The old way ! --------------------------------------------------------
    # for key, value in statistics.items():
    #     if key in index_metadata:
    #         # table.add_row(key, str(round_float_values(value, rounding_places)))
    #         table.add_row(key, str(value))
    # # The old way ! --------------------------------------------------------
    for key in index_metadata:
        if key in statistics:
            add_statistic_row(
                table=table,
                key=key,
                value=statistics[key],
            )

    # Create and display Panel with Table
    panel = Panel(table, title="Statistics", expand=False)
    console = Console()
    console.print(panel)

solar_time

Functions:

Name Description
print_solar_time_series_table

Print the solar time series results in a table format, with repeated values (like min/max time, unit)

print_solar_time_series_table

print_solar_time_series_table(
    longitude,
    timestamps,
    timezone,
    solar_time_series,
    title="True Solar Time",
    index: bool = False,
    rounding_places=ROUNDING_PLACES_DEFAULT,
    user_requested_timestamps=None,
    user_requested_timezone=None,
    group_models=False,
) -> None

Print the solar time series results in a table format, with repeated values (like min/max time, unit) placed in the caption.

Source code in pvgisprototype/cli/print/solar_time.py
def print_solar_time_series_table(
    longitude,
    timestamps,
    timezone,
    solar_time_series,
    title="True Solar Time",
    index: bool = False,
    rounding_places=ROUNDING_PLACES_DEFAULT,
    user_requested_timestamps=None,
    user_requested_timezone=None,
    group_models=False,
) -> None:
    """
    Print the solar time series results in a table format, with repeated values (like min/max time, unit)
    placed in the caption.
    """
    # Round longitude for consistent display
    longitude = round_float_values(longitude, rounding_places)
    rounded_solar_time_series = round_float_values(solar_time_series, rounding_places)

    # Define columns for the table
    columns = []
    if index:
        columns.append("Index")

    # Define the time column name based on the timezone or user request
    time_column_name = TIME_COLUMN_NAME if user_requested_timestamps is None else LOCAL_TIME_COLUMN_NAME
    columns.append(time_column_name)
    columns.append("Solar Time")

    # Extract metadata that will be placed in the caption
    caption = build_caption(
        longitude=longitude,
        latitude=None,
        rounded_table=rounded_solar_time_series,
        timezone=timezone,
        user_requested_timezone=user_requested_timezone,
    )

    for model_name, model_result in rounded_solar_time_series.items():
        model_caption = caption
        model_caption += f"\n Timing Model : [bold]{model_name}[/bold]"

        # Extract metadata for the caption
        from pvgisprototype.cli.print.helpers import get_value_or_default

        true_solar_time = model_result.get(SOLAR_TIME_NAME, {})
        if isinstance(true_solar_time, TrueSolarTime):
            solar_timing_algorithm = true_solar_time.timing_algorithm 
            unit = true_solar_time.unit
            min_time = true_solar_time.min_minutes
            max_time = true_solar_time.max_minutes

        caption = build_caption(
            longitude=longitude,
            latitude=None,
            rounded_table=rounded_solar_time_series,
            timezone=timezone,
            user_requested_timezone=user_requested_timezone,
            minimum_value=min_time,
            maximum_value=max_time,
        )

        # Create the table object
        table_obj = Table(
            *columns,
            title=title,
            box=SIMPLE_HEAD,
            show_header=True,
            header_style="bold magenta",
        )

        # Iterate over timestamps and add rows for each timestamp with the corresponding solar time
        for _index, timestamp in enumerate(timestamps):
            row = []
            if index:
                row.append(str(_index + 1))  # Add index if requested

            row.append(str(timestamp))  # Add timestamp

            # Extract solar time values for the current timestamp
            solar_time_values = true_solar_time.value
            solar_time_value = (
                f'{solar_time_values[_index]:.{rounding_places}f}' if _index < len(solar_time_values) else NOT_AVAILABLE
            )
            # row.append(model_name)  # Add model name
            row.append(solar_time_value)  # Add solar time for the given timestamp

            table_obj.add_row(*row)

        # Print the table and caption
        Console().print(table_obj)
        Console().print(Panel(model_caption, expand=False))

sparklines

Functions:

Name Description
convert_series_to_sparkline

convert_series_to_sparkline

convert_series_to_sparkline(
    series: NDArray,
    timestamps: DatetimeIndex | Timestamp,
    frequency: str,
)
Source code in pvgisprototype/cli/print/sparklines.py
def convert_series_to_sparkline(
    series: NDArray,
    timestamps: DatetimeIndex | Timestamp,
    frequency: str,
):
    """ """
    pandas_series = Series(series, timestamps)
    if (
        frequency == "SINGLE"
        or (isinstance(timestamps, DatetimeIndex) and timestamps.size == 1)
        or isinstance(timestamps, Timestamp)
    ):
        return "▁"  # Return a flat line for a single value

    yearly_sum_series = pandas_series.resample(frequency).sum()
    maximum=None
    if yearly_sum_series.all() == 0:
        maximum=1
    sparkline = sparklines(yearly_sum_series, maximum=maximum)[0]

    return sparkline

spectral_factor

Functions:

Name Description
print_spectral_factor

Print the spectral factor series in a formatted table.

print_spectral_factor_statistics

Print the spectral factor statistics in a formatted table.

print_spectral_factor

print_spectral_factor(
    timestamps,
    spectral_factor_container: Dict,
    spectral_factor_model: List,
    photovoltaic_module_type: List,
    rounding_places: int = 3,
    include_statistics: bool = False,
    title: str = "Spectral Factor",
    verbose: int = 1,
    index: bool = False,
    show_footer: bool = True,
) -> None

Print the spectral factor series in a formatted table.

Source code in pvgisprototype/cli/print/spectral_factor.py
def print_spectral_factor(
    timestamps,
    spectral_factor_container: Dict,
    spectral_factor_model: List,
    photovoltaic_module_type: List,
    rounding_places: int = 3,
    include_statistics: bool = False,
    title: str = "Spectral Factor",
    verbose: int = 1,
    index: bool = False,
    show_footer: bool = True,
) -> None:
    """Print the spectral factor series in a formatted table.

    Parameters
    ----------
    - timestamps :
        The time series timestamps.
    - spectral_factor :
        Dictionary containing spectral factor data for different models and module types.
    - spectral_factor_model :
        List of spectral factor models.
    - photovoltaic_module_type :
        List of photovoltaic module types.
    - rounding_places :
        Number of decimal places for rounding.
    - include_statistics :
        Whether to include mean, median, etc., in the output.
    - verbose : int
        Verbosity level.
    - index : bool
        Whether to show an index column.
    """
    # Initialize the table with title and formatting options
    table = Table(
        title=title,
        caption_justify="left",
        expand=False,
        padding=(0, 1),
        box=SIMPLE_HEAD,
        show_footer=show_footer,
    )
    if index:
        table.add_column("Index")

    table.add_column("Time", footer="μ" if show_footer else None)
        # Initialize dictionary to store the means for each module type
    means = {}

    # Calculate mean values for the footer
    if show_footer:
        for module_type in photovoltaic_module_type:
            model = spectral_factor_model[0]  # Assuming only one model for simplicity
            spectral_factor_series = spectral_factor_container.get(model).get(module_type).get(SPECTRAL_FACTOR_COLUMN_NAME)
            mean_value = numpy.nanmean(spectral_factor_series)
            means[module_type.value] = f"{mean_value:.{rounding_places}f}"

    # Add columns for each photovoltaic module type with optional footer
    for module_type in photovoltaic_module_type:
        footer_text = means.get(module_type.value, "") if show_footer else None
        table.add_column(f"{module_type.value}", justify="right", footer=footer_text)

    # Aggregate data for each timestamp
    for _index, timestamp in enumerate(timestamps):
        row = []

        if index:
            row.append(str(_index + 1))  # count from 1

        row.append(str(timestamp))

        for module_type in photovoltaic_module_type:
            model = spectral_factor_model[0]  # Assuming only one model for simplicity
            sm_value = spectral_factor_container.get(model).get(module_type).get(SPECTRAL_FACTOR_COLUMN_NAME)[_index]
            row.append(f"{round(sm_value, rounding_places):.{rounding_places}f}")
        table.add_row(*row)

    print()

    # Print the table if verbose is enabled
    if verbose:
        console = Console()
        console.print(table)

        # Optionally, display additional information in a panel
        if verbose > 1:
            extra_info = "Spectral Mismatch calculated for different photovoltaic module types using specified models."
            console.print(Panel(extra_info, expand=False))

print_spectral_factor_statistics

print_spectral_factor_statistics(
    spectral_factor: Dict,
    spectral_factor_model: List,
    photovoltaic_module_type: List,
    timestamps: DatetimeIndex,
    groupby: str | None = None,
    title: str = "Spectral Mismatch Statistics",
    rounding_places: int = 3,
    verbose: int = 1,
    show_footer: bool = True,
    monthly_overview: bool = False,
) -> None

Print the spectral factor statistics in a formatted table.

Source code in pvgisprototype/cli/print/spectral_factor.py
def print_spectral_factor_statistics(
    spectral_factor: Dict,
    spectral_factor_model: List,
    photovoltaic_module_type: List,
    timestamps: DatetimeIndex,
    groupby: str | None = None,
    title: str = "Spectral Mismatch Statistics",
    rounding_places: int = 3,
    verbose: int = 1,
    show_footer: bool = True,
    monthly_overview: bool = False,
) -> None:
    """
    Print the spectral factor statistics in a formatted table.

    """
    rename_monthly_output_rows = {
        "Sum of Group Means": "Yearly PV energy",
        f"Sum of {GLOBAL_INCLINED_IRRADIANCE_COLUMN_NAME}": "Yearly in-plane irradiance",
    }

    # Iterate through spectral factor models
    for model in spectral_factor_model:
        if model.value not in spectral_factor:
            print(f"Spectral factor model {model.value} not found in statistics.")
            continue

        # Create a new table for this model
        table = Table(
            title=f"{title} ({model.value})",
            caption="Spectral Factor Statistics",
            show_header=True,
            header_style="bold magenta",
            row_styles=["none", "dim"],
            box=SIMPLE_HEAD,
            highlight=True,
        )

        # Add a column for each photovoltaic module type
        table.add_column(
            "Statistic", justify="right", style="bright_blue", no_wrap=True
        )
        for module_type in photovoltaic_module_type:
            table.add_column(f"{module_type.value}",
                             # justify="right",
                             style="cyan")

        # Calculate statistics for each module type
        statistics = calculate_spectral_factor_statistics(
            spectral_factor, spectral_factor_model, photovoltaic_module_type, timestamps, rounding_places, groupby
        )

        # Basic metadata (Start, End, Count)
        basic_metadata = ["Start", "End", "Count"]
        for stat_name in basic_metadata:
            row = [stat_name]
            for module_type in photovoltaic_module_type:
                try:
                    value = statistics[model.value][module_type.value].get(stat_name, "N/A")
                    rounded_value = f"{round_float_values(value, rounding_places)}"
                except KeyError:
                    rounded_value = "N/A"
                row.append(rounded_value)
            table.add_row(*row)

        # Separate!
        table.add_row("", "")

        # Extended statistics (Min, Mean, Max, Sum, etc.)
        extended_statistics = ["Min", "Mean", "Max", "Sum", "25th Percentile", "Median", "Mode", "Variance", "Standard deviation"]
        for stat_name in extended_statistics:
            row = [stat_name]
            for module_type in photovoltaic_module_type:
                try:
                    value = statistics[model.value][module_type.value].get(stat_name, "N/A")
                    rounded_value = f"{round_float_values(value, rounding_places)}"
                except KeyError:
                    rounded_value = "N/A"
                row.append(rounded_value)
            table.add_row(*row)

        # Separate!
        table.add_row("", "")

        # Add index statistics (Time of Min, Index of Min, Time of Max, Index of Max)
        index_metadata = ["Time of Min", "Index of Min", "Time of Max", "Index of Max"]
        for stat_name in index_metadata:
            row = [stat_name]
            for module_type in photovoltaic_module_type:
                try:
                    value = statistics[model.value][module_type.value].get(stat_name, "N/A")
                    rounded_value = f"{round_float_values(value, rounding_places)}"
                except KeyError:
                    rounded_value = "N/A"
                row.append(rounded_value)
            table.add_row(*row)

        # Add a separating row after the statistics for clarity
        table.add_row("", "")

        # Groupings (Yearly, Monthly, Custom Frequency)
        time_groupings = [
            "Yearly means",
            "Monthly means",
            "Weekly means",
            "Daily means",
            "Hourly means",
        ]
        custom_freq_label = f"{groupby} means" if groupby and groupby not in time_groupings else None
        if custom_freq_label and custom_freq_label in statistics:
            custom_freq_data = statistics[custom_freq_label]
            period_count = 1
            for val in custom_freq_data:
                label = f"{groupby} Period {period_count}"
                table.add_row(label, str(val))
                period_count += 1
            table.add_row("", "")

        # Optionally add footer if show_footer is True
        if show_footer:
            table.add_row("", "")
            table.add_row("Summary", "Footer with additional info")

        # Print the table for this model
        if verbose:
            console = Console()
            console.print(table)

surface

Functions:

Name Description
build_surface_position_table
print_optimal_surface_position_output

Print surface optimisation results in a clean, minimal panel format.

print_surface_position_table

build_surface_position_table

build_surface_position_table(
    title: str | None = "Surface Position",
)
Source code in pvgisprototype/cli/print/surface.py
def build_surface_position_table(
    title: str | None = "Surface Position",
    # index: bool,
    # surface_position_data,
    # timestamps,
    # rounding_places,
    # keys_to_sum: dict,
    # keys_to_average: dict,
    # keys_to_exclude: dict,
    # time_column_name: RenderableType = "Time",
    # time_column_footer: RenderableType = SYMBOL_SUMMATION,
    # time_column_footer_style: str = "purple",
):
    """
    """
    table = Table(
        title=title,
        title_style='dim',
        # caption=caption.rstrip(', '),  # Remove trailing comma + space
        # caption_justify="left",
        # expand=False,
        # padding=(0, 1),
        # box=SIMPLE_HEAD,
        box=None,
        show_edge=False,
        pad_edge=False,
        # header_style="bold gray50",
        show_header=True,
        header_style=None,
        # show_footer=True,
        # footer_style='white',
        row_styles=["none", "dim"],
        highlight=True,
    )
    table.add_column(
        "Parameter",
        justify="right",
        style="dim",
        no_wrap=True,
    )
    table.add_column(
        "Angle",
        justify="right",
    )
    table.add_column(
        "Unit",
        # justify="left",
    )
    table.add_column(
        "Optimised",
        style="bold",
        justify="center",
    )
    table.add_column(
        "Range",
        justify="left",
        style="dim",
    )

    return table

print_optimal_surface_position_output

print_optimal_surface_position_output(
    surface_position_data: dict,
    surface_position: OptimalSurfacePosition,
    surface_orientation: bool = True,
    min_surface_orientation: float | None = None,
    max_surface_orientation: float | None = None,
    surface_tilt: bool = True,
    min_surface_tilt: float | None = None,
    max_surface_tilt: float | None = None,
    photovoltaic_power: bool = True,
    timestamps: DatetimeIndex | None = None,
    timezone: ZoneInfo = ZoneInfo("UTC"),
    title: str | None = None,
    title_power: str | None = None,
    subtitle_power: str | None = "Photovoltaic Power",
    subtitle_position: str | None = "Surface Position",
    subtitle_metadata="Metadata",
    version: bool = False,
    fingerprint: bool = False,
    rounding_places: int = 3,
) -> None

Print surface optimisation results in a clean, minimal panel format.

Parameters:

Name Type Description Default
result dict

Dictionary containing optimisation results with keys: - 'Surface Orientation': dict with 'value', 'optimal', 'unit' - 'Surface Tilt': dict with 'value', 'optimal', 'unit' - 'Mean PV Power': float - 'Unit': str (angle unit) - 'Timing': str (algorithm name) - 'Fingerprint 🆔': str (optional)

required
rounding_places int

Number of decimal places for rounding

3
Source code in pvgisprototype/cli/print/surface.py
def print_optimal_surface_position_output(
    surface_position_data: dict,
    surface_position: OptimalSurfacePosition,
    surface_orientation: bool = True,
    min_surface_orientation: float | None = None,
    max_surface_orientation: float | None = None,
    surface_tilt: bool = True,
    min_surface_tilt: float | None = None,
    max_surface_tilt: float | None = None,
    photovoltaic_power: bool = True,
    timestamps: DatetimeIndex | None = None,
    timezone: ZoneInfo = ZoneInfo('UTC'),
    title: str | None = None,  #"Optimal Position",
    title_power: str | None = None,
    subtitle_power: str | None = "Photovoltaic Power",
    subtitle_position: str | None = "Surface Position",
    subtitle_metadata = "Metadata",
    version: bool = False,
    fingerprint: bool = False,
    rounding_places: int = 3,
) -> None:
    """
    Print surface optimisation results in a clean, minimal panel format.

    Parameters
    ----------
    result : dict
        Dictionary containing optimisation results with keys:
        - 'Surface Orientation': dict with 'value', 'optimal', 'unit'
        - 'Surface Tilt': dict with 'value', 'optimal', 'unit'
        - 'Mean PV Power': float
        - 'Unit': str (angle unit)
        - 'Timing': str (algorithm name)
        - 'Fingerprint 🆔': str (optional)
    rounding_places : int, default=3
        Number of decimal places for rounding
    """
    # from devtools import debug
    # debug(surface_position)

    # Time might be Local 

    # if user_requested_timestamps is not None:
    #     time_column_name = LOCAL_TIME_COLUMN_NAME
    # else:
    #     time_column_name = TIME_COLUMN_NAME

    # if timestamps is not None:
    #     if user_requested_timezone is not None:
    #         if user_requested_timezone != ZoneInfo("UTC"):
    #             time_column_name = LOCAL_TIME_COLUMN_NAME
    #             timezone_string = f"Local Zone: [bold]{timezone}[/bold]"
    #         else:
    #             time_column_name = TIME_COLUMN_NAME

    if timezone:
        if timezone == ZoneInfo('UTC'):
            timezone_string = f"[bold]{timezone}[/bold]"
        else:
            timezone_string = f"Local Zone: [bold]{timezone}[/bold]"

    # Collect output data

    if surface_orientation:
        # orientation = surface_position_data.get('Surface Orientation', {})
        orientation = surface_position.surface_orientation
        orientation.value = round_float_values(orientation.value, rounding_places)
        orientation.optimal = "[green]✓[/green]" if orientation.optimal else "[red]✗[/red]"
        if min_surface_orientation and max_surface_orientation:
            orientation_range = f"[{min_surface_orientation}, {max_surface_orientation}]"

    if surface_tilt:
        tilt = surface_position.surface_tilt
        tilt.value = round_float_values(tilt.value, rounding_places)
        tilt.optimal = "[green]✓" if tilt.optimal else "[red]✗"
        tilt_range = f"[{min_surface_tilt}, {max_surface_tilt}]"

    if photovoltaic_power:
        minimum_power = float()
        mean_power = surface_position.mean_photovoltaic_power
        maximum_power = float()

        if mean_power is not None:
            mean_power = round_float_values(float(mean_power), rounding_places)

    # Build output table

    photovoltaic_power_table = Table(
        title=title_power,
        title_style='dim',
        # caption=caption.rstrip(', '),  # Remove trailing comma + space
        # caption_justify="left",
        # expand=False,
        # padding=(0, 1),
        # box=SIMPLE_HEAD,
        box=None,
        show_edge=False,
        pad_edge=False,
        # header_style="bold gray50",
        show_header=True,
        header_style=None,
        # show_footer=True,
        # footer_style='white',
        row_styles=["none", "dim"],
        highlight=True,
    )
    photovoltaic_power_table.add_column(
        "Statistic",
        justify="right",
        style="dim",
        no_wrap=True,
    )
    photovoltaic_power_table.add_column(
        "Power",
        justify="right",
    )
    photovoltaic_power_table.add_column(
        "Unit",
        # justify="left",
    )

    surface_position_table = build_surface_position_table(title=title)

    # Populate output table

    minimum_photovoltaic_power_row = []
    photovoltaic_power_row = []
    maximum_photovoltaic_power_row = []

    orientation_row = []
    tilt_row = []

    if surface_orientation:
        # surface_orientation_output = "Surface Orientation ⯐ "
        orientation_row.append("Orientation ⯐")
        orientation_row.append(f"{orientation.value}")
        orientation_row.append(f"{orientation.unit}")
        orientation_row.append(f"{orientation.optimal}")

        if min_surface_orientation and max_surface_orientation:
            orientation_row.append(orientation_range)

        surface_position_table.add_row(*orientation_row)

    if surface_tilt:
        tilt_row.append("Tilt ⯐")
        tilt_row.append(f"{tilt.value}")
        tilt_row.append(f"{tilt.unit}")
        tilt_row.append(f"{tilt.optimal}")

        if tilt_range:
            tilt_row.append(tilt_range)

        surface_position_table.add_row(*tilt_row)

    if photovoltaic_power:
        minimum_photovoltaic_power_row.append("Min")
        minimum_photovoltaic_power_row.append(f"[dim]{minimum_power}[/dim]")
        minimum_photovoltaic_power_row.append('W')
        photovoltaic_power_table.add_row(*minimum_photovoltaic_power_row)

        photovoltaic_power_row.append("Mean")
        photovoltaic_power_row.append(f"[bold yellow]{mean_power}[/bold yellow]")
        photovoltaic_power_row.append('W')
        photovoltaic_power_table.add_row(*photovoltaic_power_row)

        maximum_photovoltaic_power_row.append("Max")
        maximum_photovoltaic_power_row.append(f"[dim]{maximum_power}[/dim]")
        maximum_photovoltaic_power_row.append('W')
        photovoltaic_power_table.add_row(*maximum_photovoltaic_power_row)

    # Metadata

    metadata_table = Table(
        box=None,
        show_header=False,
        show_edge=False,
        pad_edge=False,
    )
    metadata_table.add_column(
        justify="right",
        style="none",
        no_wrap=True,
    )
    metadata_table.add_column(justify="left")

    # Build Panels

    time_table = build_time_table()
    frequency, frequency_label = infer_frequency_from_timestamps(timestamps)
    time_table.add_row(
        str(timestamps.strftime("%Y-%m-%d %H:%M").values[0]),
        str(frequency) if frequency and frequency != "Single" else "-",
        str(timestamps.strftime("%Y-%m-%d %H:%M").values[-1]),
        str(timezone_string),
    )
    time_panel = build_time_panel(time_table, padding=(0, 1, 2, 1))

    power_panel = Panel(
        photovoltaic_power_table,
        # title="Optimisation Results",
        subtitle=subtitle_power,
        subtitle_align='right',
        # box=HORIZONTALS,
        # box=SIMPLE_HEAD,
        safe_box=True,
        style="",
        border_style='dim',
        expand=False,
        padding=(0, 1),
    )

    position_panel = Panel(
        surface_position_table,
        # title="Optimisation Results",
        subtitle=subtitle_position,
        subtitle_align='right',
        # box=HORIZONTALS,
        # box=SIMPLE_HEAD,
        safe_box=True,
        style="",
        border_style='dim',
        expand=False,
        padding=(0, 1, 1, 1),
    )

    # Metadata Panel

    # Timing algorithm

    timing = surface_position_data.get("Timing")
    if timing:
        metadata_table.add_row("Timing :", f"[bold]{timing}[/bold]")

    # Version & Fingerprint

    fingerprint = retrieve_fingerprint(dictionary=surface_position_data)
    version_and_fingerprint_and_column = build_version_and_fingerprint_columns(
        version=version,
        fingerprint=fingerprint,
    )

    metadata_panel = Panel(
        metadata_table,
        subtitle=f"[dim]{subtitle_metadata}[/dim]",
        box=ROUNDED,
        style="dim",
        border_style="dim",
        expand=False,
        padding=(0, 1),
    )

    panels = []
    panels.append(power_panel)
    panels.append(position_panel)
    panels.append(time_panel)
    panels.append(metadata_panel)

    # Print columns

    columns = Columns(
        panels,
        # expand=False,
        # equal=True,
        # padding=2,
    )
    console.print(columns)

print_surface_position_table

print_surface_position_table(
    surface_position: dict,
    longitude,
    latitude,
    timezone,
    title="Surface Position",
    version: bool = False,
    fingerprint: bool = False,
    rounding_places=ROUNDING_PLACES_DEFAULT,
)
Source code in pvgisprototype/cli/print/surface.py
def print_surface_position_table(
    surface_position: dict,
    longitude,
    latitude,
    timezone,
    title="Surface Position",
    version: bool = False,
    fingerprint: bool = False,
    rounding_places=ROUNDING_PLACES_DEFAULT,
):
    """
    """
    panels = []
    caption = build_simple_caption(
        longitude,
        latitude,
        surface_position,
        timezone,
        user_requested_timezone=None,
    )
    # then : Create a Legend table for the symbols in question
    legend = build_legend_table(
        dictionary=surface_position,
        caption=caption,
        show_header=False,
        box=None,
    )
    caption_panel = Panel(
        caption,
        subtitle="[gray]Reference[/gray]",
        subtitle_align="right",
        border_style="dim",
        expand=False
    )
    legend_panel = Panel(
        legend,
        subtitle="[dim]Legend[/dim]",
        subtitle_align="right",
        border_style="dim",
        expand=False,
        padding=(0,1),
        # style="dim",
    )
    # surface position Panel
    table = Table(
        box=None,
        show_header=False,
        show_edge=False,
        pad_edge=False,
    )
    table.add_column(justify="right", style="none", no_wrap=True)
    table.add_column(justify="left")

    table.add_row(f"{LATITUDE_COLUMN_NAME} :", f"[bold]{latitude}[/bold]")
    table.add_row(f"{LONGITUDE_COLUMN_NAME} :", f"[bold]{longitude}[/bold]")
    # table.add_row("Time :", f"{timestamp[0]}")
    table.add_row("Time zone :", f"{timezone}")

    longest_label_length = max(len(key) for key in surface_position.keys())
    surface_position_keys = {
        SURFACE_ORIENTATION_NAME,
        SURFACE_TILT_NAME,
        ANGLE_UNIT_NAME,
        INCIDENCE_DEFINITION,
        UNIT_NAME,
    }

    for key, value in surface_position.items():
        if key in surface_position_keys:
            padded_key = f"{key} :".ljust(longest_label_length + 3, " ")
            if key == INCIDENCE_DEFINITION:
                value = f"[yellow]{value}[/yellow]"
            table.add_row(padded_key, str(value))

    position_panel = Panel(
        table,
        title="Surface Position",
        box=HORIZONTALS,
        style="",
        expand=False,
        padding=(0, 2),
    )
    panels.append(position_panel)
    version_and_fingerprint_and_column = build_version_and_fingerprint_columns(
        version=version,
        fingerprint=fingerprint,
    )

    # Use Columns to place them side-by-side
    from rich.columns import Columns

    console.print(Columns([
            caption_panel,
            legend_panel,
        ]))
    console.print(version_and_fingerprint_and_column)

symbols

Functions:

Name Description
create_symbols_table
print_pvgis_symbols

create_symbols_table

create_symbols_table()
Source code in pvgisprototype/cli/print/symbols.py
def create_symbols_table():
    """
    """
    main_table = Table(
        title='Symbol Descriptions',
        caption='This table categorizes different symbols used throughout the system.',
        show_header=False,
        box=SIMPLE_HEAD,
        highlight=True,
    )
    for category, symbols in SYMBOL_GROUPS_DESCRIPTIONS.items():
        category_table = Table(box=SIMPLE_HEAD)
        category_table.add_column("Symbol", justify="right", style="bright_blue", no_wrap=True)
        category_table.add_column("Description", style="cyan")

        for symbol, description in symbols.items():
            category_table.add_row(f'[bold]{symbol}[/bold]', description)

        main_table.add_row(category)
        main_table.add_row(category_table)
        main_table.add_row("")

    return main_table

print_pvgis_symbols

print_pvgis_symbols()
Source code in pvgisprototype/cli/print/symbols.py
def print_pvgis_symbols():
    """
    """
    # console = Console(record=True, width=150)
    console = Console()
    # console.print(create_symbols_table(), markup=True, highlight=False)
    panels = layout_panels_with_categories(SYMBOL_GROUPS_DESCRIPTIONS)
    console.print(panels, markup=True, highlight=False)

time

Functions:

Name Description
build_time_panel
build_time_table
populate_time_table

build_time_panel

build_time_panel(
    time_table,
    safe_box: bool = True,
    expand: bool = False,
    padding: tuple = (0, 2),
) -> Panel
Source code in pvgisprototype/cli/print/time.py
def build_time_panel(
    time_table,
    safe_box: bool = True,
    expand: bool = False,
    padding: tuple = (0, 2),
) -> Panel:
    """ """
    return Panel(
        time_table,
        # title="Time",
        # subtitle="Time",
        # subtitle_align="right",
        safe_box=safe_box,
        border_style="dim",
        expand=expand,
        padding=padding,
    )

build_time_table

build_time_table() -> Table
Source code in pvgisprototype/cli/print/time.py
def build_time_table() -> Table:
    """ """
    time_table = Table(
        box=None,
        show_header=True,
        header_style=None,
        show_edge=False,
        pad_edge=False,
    )
    time_table.add_column("Start", justify="left", style="bold")
    time_table.add_column("Every", justify="left", style="dim bold")
    time_table.add_column("End", justify="left", style="dim bold")
    time_table.add_column("Zone", justify="left", style="dim bold")

    return time_table

populate_time_table

populate_time_table(
    table: Table,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
) -> Table
Source code in pvgisprototype/cli/print/time.py
def populate_time_table(
        table: Table,
        timestamps: DatetimeIndex,
        timezone: ZoneInfo,
        ) -> Table:
    """
    """
    frequency, frequency_label = infer_frequency_from_timestamps(timestamps)
    table.add_row(
        str(timestamps.strftime("%Y-%m-%d %H:%M").values[0]),
        str(frequency) if frequency and frequency != 'Single' else '-',
        str(timestamps.strftime("%Y-%m-%d %H:%M").values[-1]),
        str(timezone),
    )

    return table

version

Functions:

Name Description
build_pvgis_version_panel

build_pvgis_version_panel

build_pvgis_version_panel(
    prefix_text: str = "PVGIS v6",
    justify_text: JustifyMethod = "center",
    style_text: str = "white dim",
    border_style: str = "dim",
    padding: tuple = (0, 2),
) -> Panel
Source code in pvgisprototype/cli/print/version.py
def build_pvgis_version_panel(
    prefix_text: str = "PVGIS v6",
    justify_text: JustifyMethod = "center",
    style_text: str = "white dim",
    border_style: str = "dim",
    padding: tuple = (0, 2),
) -> Panel:
    """ """
    from pvgisprototype._version import __version__

    pvgis_version = Text(
        f"{prefix_text} ({__version__})",
        justify=justify_text,
        style=style_text,
    )
    return Panel(
        pvgis_version,
        # subtitle="[reverse]Fingerprint[/reverse]",
        # subtitle_align="right",
        border_style=border_style,
        # style="dim",
        expand=False,
        padding=padding,
    )

version_and_fingerprint

Functions:

Name Description
build_version_and_fingerprint_columns

Combine software version and fingerprint panels into a single Columns

build_version_and_fingerprint_panels

Dynamically build panels based on available data.

build_version_and_fingerprint_columns

build_version_and_fingerprint_columns(
    version: bool = False, fingerprint: bool = False
) -> Columns

Combine software version and fingerprint panels into a single Columns object.

Source code in pvgisprototype/cli/print/version_and_fingerprint.py
def build_version_and_fingerprint_columns(
    version:bool = False,
    fingerprint: bool = False,
) -> Columns:
    """Combine software version and fingerprint panels into a single Columns
    object."""
    version_and_fingeprint_panels = build_version_and_fingerprint_panels(
        version=version,
        fingerprint=fingerprint,
    )

    return Columns(version_and_fingeprint_panels, expand=False, padding=2)

build_version_and_fingerprint_panels

build_version_and_fingerprint_panels(
    version: bool = False, fingerprint: bool = False
) -> list[Panel]

Dynamically build panels based on available data.

Source code in pvgisprototype/cli/print/version_and_fingerprint.py
def build_version_and_fingerprint_panels(
    version:bool = False,
    fingerprint: bool = False,
) -> list[Panel]:
    """Dynamically build panels based on available data."""
    # Always yield version panel
    panels = []
    if version:
        panels.append(build_pvgis_version_panel())
    # Yield fingerprint panel only if fingerprint is provided
    if fingerprint:
        panels.append(build_fingerprint_panel(fingerprint))

    return panels

rich_help_panel_names

Structural components of the command line interface

series

Modules:

Name Description
inspect
introduction
plot
resample
select
uniplot

inspect

Functions:

Name Description
inspect_xarray_supported_data

Select location series

inspect_xarray_supported_data

inspect_xarray_supported_data(
    time_series: Annotated[
        Path, typer_argument_time_series
    ],
    encodings: bool = False,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
)

Select location series

Source code in pvgisprototype/cli/series/inspect.py
def inspect_xarray_supported_data(
    time_series: Annotated[Path, typer_argument_time_series],
    encodings: bool = False,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
):
    """Select location series"""
    time_series_xarray = read_data_array_or_set(
            input_data=time_series,
            verbose=verbose,
    )
    print(time_series_xarray)

    if encodings:
        print(time_series_xarray.encoding)

introduction

Functions:

Name Description
series_introduction

A short introduction on the series command

series_introduction

series_introduction()

A short introduction on the series command

Source code in pvgisprototype/cli/series/introduction.py
def series_introduction():
    """A short introduction on the series command"""
    introduction = """
    The [code]series[/code] command is a convenience wrapper around Xarray's
    data processing capabilities.

    Explain [bold cyan]timestamps[/bold cyan].

    And more ...

    """

    note = """
    Timestamps are retrieved from the input data series. If the series are not
    timestamped, then stamps are generated based on the user requested
    combination of a three out of the four relevant parameters : `start-time`,
    `end-time`, `frequency` and `period`.

    """
    from rich.panel import Panel

    note_in_a_panel = Panel(
        "[italic]{}[/italic]".format(note),
        title="[bold cyan]Note[/bold cyan]",
        width=78,
    )
    from rich.console import Console

    console = Console()
    # introduction.wrap(console, 30)
    console.print(introduction)
    console.print(note_in_a_panel)

plot

Functions:

Name Description
plot

Plot selected time series

plot

plot(
    time_series: Annotated[
        Path, typer_argument_time_series
    ],
    longitude: Annotated[
        float, typer_argument_longitude_in_degrees
    ],
    latitude: Annotated[
        float, typer_argument_latitude_in_degrees
    ],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_naive_timestamps
    ] = str(now()),
    start_time: Annotated[
        Timestamp | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        Timestamp | None, typer_option_end_time
    ] = None,
    convert_longitude_360: Annotated[
        bool, typer_option_convert_longitude_360
    ] = False,
    variable: Annotated[
        str | None, typer_option_data_variable
    ] = None,
    default_dimension: Annotated[
        str, "Default dimension"
    ] = "time",
    ask_for_dimension: Annotated[
        bool, "Ask to plot a specific dimension"
    ] = True,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    output_filename: Annotated[
        Path, typer_option_output_filename
    ] = None,
    variable_name_as_suffix: Annotated[
        bool, typer_option_variable_name_as_suffix
    ] = True,
    width: Annotated[int, "Width for the plot"] = 16,
    height: Annotated[int, "Height for the plot"] = 3,
    tufte_style: Annotated[
        bool, typer_option_tufte_style
    ] = False,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    data_source: Annotated[
        str,
        Option(
            help="Data source text to print in the footer of the plot."
        ),
    ] = "",
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = VERBOSE_LEVEL_DEFAULT,
)

Plot selected time series

Source code in pvgisprototype/cli/series/plot.py
def plot(
    time_series: Annotated[Path, typer_argument_time_series],
    longitude: Annotated[float, typer_argument_longitude_in_degrees],
    latitude: Annotated[float, typer_argument_latitude_in_degrees],
    timestamps: Annotated[DatetimeIndex, typer_argument_naive_timestamps] = str(Timestamp.now()),
    start_time: Annotated[
        Timestamp | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        Timestamp | None, typer_option_end_time
    ] = None,  # Used by a callback function
    convert_longitude_360: Annotated[bool, typer_option_convert_longitude_360] = False,
    variable: Annotated[str | None, typer_option_data_variable] = None,
    default_dimension: Annotated[str, 'Default dimension'] = 'time',
    ask_for_dimension: Annotated[bool, "Ask to plot a specific dimension"] = True,
    # slice_options: Annotated[bool, "Slice data dimensions"] = False,
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    output_filename: Annotated[Path, typer_option_output_filename] = None,
    variable_name_as_suffix: Annotated[
        bool, typer_option_variable_name_as_suffix
    ] = True,
    width: Annotated[int, "Width for the plot"] = 16,
    height: Annotated[int, "Height for the plot"] = 3,
    tufte_style: Annotated[bool, typer_option_tufte_style] = False,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    data_source: Annotated[str, typer.Option(help="Data source text to print in the footer of the plot.")] = '',
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = VERBOSE_LEVEL_DEFAULT,
):
    """Plot selected time series"""
    data_array = select_time_series(
        time_series=time_series,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        start_time=start_time,
        end_time=end_time,
        variable=variable,
        # convert_longitude_360=convert_longitude_360,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        # in_memory=in_memory,
        verbose=verbose,
        log=log,
    )
    try:
        plot_series(
            data_array=data_array,
            time=timestamps,
            default_dimension=default_dimension,
            ask_for_dimension=ask_for_dimension,
            # slice_options=slice_options,
            figure_name=output_filename.name,
            save_path=output_filename.parent,
            # add_offset=add_offset,
            variable_name_as_suffix=variable_name_as_suffix,
            tufte_style=tufte_style,
            width=width,
            height=height,
            resample_large_series=resample_large_series,
            data_source=data_source,
            fingerprint=fingerprint,
        )
    except Exception as exception:
        logger.error(
                f"{ERROR_IN_PLOTTING_DATA} : {exception}",
                alt=f"{ERROR_IN_PLOTTING_DATA} : {exception}"
                )
        raise SystemExit(33)

resample

Functions:

Name Description
resample

Time-based groupby of solar radiation and PV output power time series over a location.

resample

resample(indexer: str | None = None)

Time-based groupby of solar radiation and PV output power time series over a location.

For example: - solar radiation on horizontal and inclined planes - Direct Normal Irradiation (DNI) and more in various - the daily variation in the clear-sky radiation

  • hourly
  • daily
  • monthly

Parameters:

Name Type Description Default
indexer str | None
None
Source code in pvgisprototype/cli/series/resample.py
def resample(
    indexer: str | None = None,  # The offset string or object representing target conversion.
    # or : Mapping from a date-time dimension to resample frequency [1]
):
    """Time-based groupby of solar radiation and PV output power time series over a location.

    For example:
    - solar radiation on horizontal and inclined planes
    - Direct Normal Irradiation (DNI) and more in various
    - the daily variation in the clear-sky radiation

    - hourly
    - daily
    - monthly

    Parameters
    ----------
    indexer: str
    """
    pass

select

Functions:

Name Description
select

Select location series

select_fast

Bare read & write

select_sarah

Select location series

warn_for_negative_longitude

Warn for negative longitude value

write_to_netcdf

Save to NetCDF with lon and lat dimensions of length 1.

select

select(
    time_series: Annotated[
        Path, typer_argument_time_series
    ],
    longitude: Annotated[
        float, typer_argument_longitude_in_degrees
    ],
    latitude: Annotated[
        float, typer_argument_latitude_in_degrees
    ],
    time_series_2: Annotated[
        Path, typer_option_time_series
    ] = None,
    timestamps: Annotated[
        DatetimeIndex | None,
        typer_argument_naive_timestamps,
    ] = str(now()),
    start_time: Annotated[
        Timestamp | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        Timestamp | None, typer_option_end_time
    ] = None,
    convert_longitude_360: Annotated[
        bool, typer_option_convert_longitude_360
    ] = False,
    variable: Annotated[
        str | None, typer_option_data_variable
    ] = None,
    variable_2: Annotated[
        str | None, typer_option_data_variable
    ] = None,
    coordinate: Annotated[
        str, typer_option_wavelength_column_name
    ] = None,
    filter_coordinate: Annotated[
        bool,
        Option(
            help="Limit the range of input data by filtering the requested Xarray `coordinate`. See options `minimum`, `maximum`.",
            rich_help_panel=rich_help_panel_spectrum,
        ),
    ] = False,
    minimum: Annotated[
        float | None,
        typer_option_minimum_spectral_irradiance_wavelength,
    ] = None,
    maximum: Annotated[
        float | None,
        typer_option_maximum_spectral_irradiance_wavelength,
    ] = None,
    neighbor_lookup: Annotated[
        MethodForInexactMatches | None,
        typer_option_nearest_neighbor_lookup,
    ] = None,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    output_filename: Annotated[
        Path, typer_option_output_filename
    ] = None,
    variable_name_as_suffix: Annotated[
        bool, typer_option_variable_name_as_suffix
    ] = True,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    uniplot: Annotated[
        bool, typer_option_uniplot
    ] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    lines: Annotated[
        bool, typer_option_uniplot_lines
    ] = True,
    title: Annotated[
        str | None, typer_option_uniplot_title
    ] = "Selected data",
    unit: Annotated[
        str, typer_option_uniplot_unit
    ] = UNIT_NAME,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = VERBOSE_LEVEL_DEFAULT,
    data_source: Annotated[
        str,
        Option(
            help="Data source text to print in the footer of the plot."
        ),
    ] = "",
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
)

Select location series

Source code in pvgisprototype/cli/series/select.py
def select(
    time_series: Annotated[Path, typer_argument_time_series],
    longitude: Annotated[float, typer_argument_longitude_in_degrees],
    latitude: Annotated[float, typer_argument_latitude_in_degrees],
    time_series_2: Annotated[Path, typer_option_time_series] = None,
    timestamps: Annotated[DatetimeIndex | None, typer_argument_naive_timestamps] = str(Timestamp.now()),
    start_time: Annotated[
        Timestamp | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        Timestamp | None, typer_option_end_time
    ] = None,  # Used by a callback function
    convert_longitude_360: Annotated[bool, typer_option_convert_longitude_360] = False,
    variable: Annotated[str | None, typer_option_data_variable] = None,
    variable_2: Annotated[str | None, typer_option_data_variable] = None,
    coordinate: Annotated[
        str,
        typer_option_wavelength_column_name,  # Update Me
    ] = None,
    filter_coordinate: Annotated[
        bool,
        typer.Option(
            help="Limit the range of input data by filtering the requested Xarray `coordinate`. See options `minimum`, `maximum`.",
            rich_help_panel=rich_help_panel_spectrum,
        ),
    ] = False,
    minimum: Annotated[
        float | None, typer_option_minimum_spectral_irradiance_wavelength  # Update Me
    ] = None,
    maximum: Annotated[
        float | None, typer_option_maximum_spectral_irradiance_wavelength  # Update Me
    ] = None,
    neighbor_lookup: Annotated[
        MethodForInexactMatches | None, typer_option_nearest_neighbor_lookup
    ] = None, # NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    output_filename: Annotated[
        Path, typer_option_output_filename
    ] = None,
    variable_name_as_suffix: Annotated[
        bool, typer_option_variable_name_as_suffix
    ] = True,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    uniplot: Annotated[bool, typer_option_uniplot] = UNIPLOT_FLAG_DEFAULT,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    lines: Annotated[bool, typer_option_uniplot_lines] = True,
    title: Annotated[str | None, typer_option_uniplot_title] = 'Selected data',
    unit: Annotated[str, typer_option_uniplot_unit] = UNIT_NAME,  # " °C")
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
    log: Annotated[int, typer_option_log] = VERBOSE_LEVEL_DEFAULT,
    data_source: Annotated[str, typer.Option(help="Data source text to print in the footer of the plot.")] = '',
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    # quick_response_code: Annotated[
    #     QuickResponseCode, typer_option_quick_response
    # ] = QuickResponseCode.NoneValue,
):
    """Select location series"""
    if convert_longitude_360:
        longitude = longitude % 360
    warn_for_negative_longitude(longitude)

    if not variable:
        dataset = open_dataset(time_series)
        # ----------------------------------------------------- Review Me ----    
        #
        if len(dataset.data_vars) >= 2:
            variables = list(dataset.data_vars.keys())
            print(f"The dataset contains more than one variable : {variables}")
            variable = typer.prompt(
                "Please specify the variable you are interested in from the above list"
            )
        else:
            variable = list(dataset.data_vars)
        #
        # ----------------------------------------------------- Review Me ----    
    location_time_series = select_time_series(
        time_series=time_series,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        start_time=start_time,
        end_time=end_time,
        # convert_longitude_360=convert_longitude_360,
        variable=variable,
        coordinate=coordinate,
        minimum=minimum,
        maximum=maximum,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        in_memory=in_memory,
        # variable_name_as_suffix=variable_name_as_suffix,
        verbose=verbose,
        log=log,
    )
    if resample_large_series:
        location_time_series = location_time_series.resample(time="1M").mean()
    location_time_series_2 = select_time_series(
        time_series=time_series_2,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        start_time=start_time,
        end_time=end_time,
        # convert_longitude_360=convert_longitude_360,
        variable=variable_2,
        coordinate=coordinate,
        minimum=minimum,
        maximum=maximum,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        in_memory=in_memory,
        # variable_name_as_suffix=variable_name_as_suffix,
        verbose=verbose,
        log=log,
    )
    if resample_large_series:
        location_time_series_2 = location_time_series_2.resample(time="1M").mean()

    results = {
        location_time_series.name: location_time_series.to_numpy(),
    }
    if location_time_series_2 is not None:
        more_results = {
            location_time_series_2.name: (
                location_time_series_2.to_numpy()
                if location_time_series_2 is not None
                else None
            )
        }
        results = results | more_results

    # if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
    #     debug(locals())

    if not quiet:
        if verbose > 0:
            # special case!
            if location_time_series is not None and timestamps is None:
                timestamps = location_time_series.time.to_numpy()


            if isinstance(location_time_series, DataArray):
                from pvgisprototype.cli.print.irradiance.table import print_irradiance_xarray

                print_irradiance_xarray(
                    location_time_series=location_time_series,
                    longitude=longitude,
                    latitude=latitude,
                    # elevation=elevation,
                    title=title,
                    rounding_places=rounding_places,
                    verbose=verbose,
                    # index=index,
                )
                if isinstance(location_time_series_2, DataArray):
                    print_irradiance_xarray(
                        location_time_series=location_time_series_2,
                        longitude=longitude,
                        latitude=latitude,
                        # elevation=elevation,
                        title=title,
                        rounding_places=rounding_places,
                        verbose=verbose,
                        # index=index,
                    )
            else:
                from pvgisprototype.cli.print.irradiance.data import print_irradiance_table_2

                print_irradiance_table_2(
                    longitude=longitude,
                    latitude=latitude,
                    timestamps=timestamps,
                    dictionary=results,
                    title=title,
                    rounding_places=rounding_places,
                    verbose=verbose,
                )
                # if location_time_series_2 is not None:
                #     print_irradiance_table_2(
                #         longitude=longitude,
                #         latitude=latitude,
                #         timestamps=timestamps,
                #         dictionary=results,
                #         title=title,
                #         rounding_places=rounding_places,
                #         verbose=verbose,
                #     )
        else:
            flat_list = location_time_series.to_numpy().flatten().astype(str)
            csv_str = ",".join(flat_list)
            print(csv_str)

    # statistics after echoing series which might be Long!

    if statistics:
        from pvgisprototype.api.series.statistics import print_series_statistics

        print_series_statistics(
            data_array=location_time_series,
            timestamps=timestamps,
            groupby=groupby,
            title="Selected series",
            rounding_places=rounding_places,
        )
    if fingerprint:
        from pvgisprototype.cli.print.fingerprint import print_finger_hash

        print_finger_hash(dictionary=results)
    if uniplot:
        import os

        terminal_columns, _ = os.get_terminal_size()  # we don't need lines!
        terminal_length = int(terminal_columns * terminal_width_fraction)
        from functools import partial

        from uniplot import plot as default_plot

        plot = partial(default_plot, width=terminal_length)
        if isinstance(location_time_series, DataArray) and location_time_series.size == 1:
            print(
                f"{exclamation_mark} I [red]cannot[/red] plot a single scalar value {location_time_series.item()}!"
            )
            raise typer.Abort()

        if not isinstance(location_time_series, DataArray):
            print("Selected variable did not return a DataArray. Check your selection.")
            return

        if isinstance(location_time_series, DataArray):
            supertitle = getattr(location_time_series, "long_name", "Untitled")
            title += f'  at ({longitude}, {latitude})'
            label = getattr(location_time_series, "name", None)
            label_2 = (
                getattr(location_time_series_2, "name", None)
                if isinstance(location_time_series_2, DataArray)
                else None
            )
            data_source_text = ''
            if data_source:
                data_source_text = f" · {data_source}"

            if fingerprint:
                from pvgisprototype.core.hashing import generate_hash
                data_source_text += f" · Fingerprint : {generate_hash(location_time_series)}"

            if label_2:
                label_2 += data_source_text

            else:
                label += data_source_text

            unit = getattr(location_time_series, "units", None)
            if coordinate in location_time_series.coords:
                y_series = (
                    [location_time_series, location_time_series_2]
                    if location_time_series_2 is not None
                    else location_time_series
                )

                if location_time_series.time.size == 1:
                    x_series = location_time_series[coordinate].values
                    title += f'  on {str(timestamps[0])}'
                    x_unit = ' ' + getattr(location_time_series[coordinate], 'units', '')

                else:
                    x_unit = ''
                    if location_time_series[coordinate].size == 1:
                        # Case 2: One level of coordinate, time on x-axis
                        # x_series = location_time_series.time.values
                        # x_series = [[str(timestamp)] for timestamp in location_time_series['time'].values]
                        x_series = location_time_series.time.values
                        y_series = y_series.values.flatten()
                        title += f'  at {location_time_series[coordinate].item()}'
                    else:
                        # Case 3: Multiple levels of the coordinate and multiple timestamps
                        # # x_series = [location_time_series[coordinate].values] * len(y_series)
                        # x_series = [location_time_series[coordinate].values]
                        for level in location_time_series[coordinate].values:
                            level_data = location_time_series.sel({coordinate: level})
                            x_series = level_data.time.values
                            y_series = level_data.values.flatten()  # Flatten to 1D
                            plot_title = f'{title}  at {level} {getattr(location_time_series[coordinate], "units", "")}'
                            plot_x_unit = 'time'
                            plot(
                                xs=x_series,
                                ys=y_series,
                                legend_labels=[label],
                                lines=lines,
                                title=plot_title,
                                x_unit=plot_x_unit,
                                y_unit=' ' + str(unit),
                            )
                        return
                plot(
                    # x=location_time_series,
                    xs=x_series,
                    ys=y_series,
                    legend_labels=[label, label_2],
                    lines=lines,
                    title=title if title else supertitle,
                    x_unit=x_unit,
                    y_unit=' ' + str(unit),
                    # force_ascii=True,
                )
            else:
                # plot over time
                plot(
                    # x=location_time_series,
                    # xs=location_time_series,
                    ys=[location_time_series, location_time_series_2] if location_time_series_2 is not None else location_time_series,
                    legend_labels=[label, label_2],
                    lines=lines,
                    title=title if title else supertitle,
                    y_unit=" " + str(unit),
                    # force_ascii=True,
                )

    output_handlers = {
        ".nc": lambda location_time_series, path: write_to_netcdf(
            location_time_series=location_time_series,
            path=path,
            longitude=longitude,
            latitude=latitude
        ),
        ".csv": lambda location_time_series, path: to_csv(
            x=location_time_series, path=path
        ),
    }
    if output_filename:
        extension = output_filename.suffix.lower()
        if extension in output_handlers:
            output_handlers[extension](location_time_series, output_filename)
        else:
            raise ValueError(f"Unsupported file extension: {extension}")

select_fast

select_fast(
    time_series: Annotated[
        Path, typer_argument_time_series
    ],
    longitude: Annotated[
        float, typer_argument_longitude_in_degrees
    ],
    latitude: Annotated[
        float, typer_argument_latitude_in_degrees
    ],
    time_series_2: Annotated[
        Path, typer_option_time_series
    ] = None,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = 0.1,
    output_filename: Annotated[
        Path | None, typer_option_output_filename
    ] = None,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
)

Bare read & write

Source code in pvgisprototype/cli/series/select.py
def select_fast(
    time_series: Annotated[Path, typer_argument_time_series],
    longitude: Annotated[float, typer_argument_longitude_in_degrees],
    latitude: Annotated[float, typer_argument_latitude_in_degrees],
    time_series_2: Annotated[Path, typer_option_time_series] = None,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = 0.1,  # Customize default if needed
    # in_memory: Annotated[bool, typer_option_in_memory] = False,
    output_filename: Annotated[Path | None, typer_option_output_filename] = None,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
):
    """Bare read & write"""
    try:
        series = open_dataarray(time_series).sel(
            lon=longitude, lat=latitude, method="nearest"
        )
        if time_series_2:
            series_2 = open_dataarray(time_series_2).sel(
                lon=longitude, lat=latitude, method="nearest"
            )
        # Is .nc needed in the context of this command ? ---------------------
        output_handlers = {
            # ".nc": lambda location_time_series, path: location_time_series.to_netcdf(path),
            ".csv": lambda location_time_series, path: to_csv(
                x=location_time_series, path=path
            ),
        }
        if output_filename:
            extension = output_filename.suffix.lower()
            if extension in output_handlers:
                output_handlers[extension](location_time_series, output_filename)
            else:
                raise ValueError(f"Unsupported file extension: {extension}")
        print("Done.-")
    except Exception as e:
        print(f"An error occurred: {e}")

select_sarah

select_sarah(
    time_series: Annotated[
        Path, typer_argument_time_series
    ],
    longitude: Annotated[
        float, typer_argument_longitude_in_degrees
    ],
    latitude: Annotated[
        float, typer_argument_latitude_in_degrees
    ],
    time_series_2: Annotated[
        Path, typer_option_time_series
    ] = None,
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now()),
    start_time: Annotated[
        Timestamp | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        Timestamp | None, typer_option_end_time
    ] = None,
    convert_longitude_360: Annotated[
        bool, typer_option_convert_longitude_360
    ] = False,
    variable: Annotated[
        str | None, typer_option_data_variable
    ] = None,
    variable_2: Annotated[
        str | None, typer_option_data_variable
    ] = None,
    wavelength_column: Annotated[
        str, typer_option_wavelength_column_name
    ] = WAVELENGTHS_CSV_COLUMN_NAME_DEFAULT,
    limit_spectral_range: Annotated[
        bool,
        Option(
            help="Limit the spectral range of the irradiance input data. Default for `spectral_mismatch_model = Pelland`"
        ),
    ] = True,
    min_wavelength: Annotated[
        float,
        typer_option_minimum_spectral_irradiance_wavelength,
    ] = MIN_WAVELENGTH,
    max_wavelength: Annotated[
        float,
        typer_option_maximum_spectral_irradiance_wavelength,
    ] = MAX_WAVELENGTH,
    neighbor_lookup: Annotated[
        MethodForInexactMatches,
        typer_option_nearest_neighbor_lookup,
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[
        bool, typer_option_in_memory
    ] = IN_MEMORY_FLAG_DEFAULT,
    statistics: Annotated[
        bool, typer_option_statistics
    ] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[
        str | None, typer_option_groupby
    ] = GROUPBY_DEFAULT,
    output_filename: Annotated[
        Path | None, typer_option_output_filename
    ] = None,
    variable_name_as_suffix: Annotated[
        bool, typer_option_variable_name_as_suffix
    ] = True,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = VERBOSE_LEVEL_DEFAULT,
)

Select location series

Source code in pvgisprototype/cli/series/select.py
def select_sarah(
    time_series: Annotated[Path, typer_argument_time_series],
    longitude: Annotated[float, typer_argument_longitude_in_degrees],
    latitude: Annotated[float, typer_argument_latitude_in_degrees],
    time_series_2: Annotated[Path, typer_option_time_series] = None,
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(Timestamp.now()),
    start_time: Annotated[
        Timestamp | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        Timestamp | None, typer_option_end_time
    ] = None,  # Used by a callback function
    convert_longitude_360: Annotated[bool, typer_option_convert_longitude_360] = False,
    variable: Annotated[str | None, typer_option_data_variable] = None,
    variable_2: Annotated[str | None, typer_option_data_variable] = None,
    wavelength_column: Annotated[
        str,
        typer_option_wavelength_column_name,
    ] = WAVELENGTHS_CSV_COLUMN_NAME_DEFAULT,
    limit_spectral_range: Annotated[
            bool,
            typer.Option(help="Limit the spectral range of the irradiance input data. Default for `spectral_mismatch_model = Pelland`")
            ] = True,
    min_wavelength: Annotated[
        float, typer_option_minimum_spectral_irradiance_wavelength
    ] = MIN_WAVELENGTH,
    max_wavelength: Annotated[
        float, typer_option_maximum_spectral_irradiance_wavelength
    ] = MAX_WAVELENGTH,
    neighbor_lookup: Annotated[
        MethodForInexactMatches, typer_option_nearest_neighbor_lookup
    ] = NEIGHBOR_LOOKUP_DEFAULT,
    tolerance: Annotated[float | None, typer_option_tolerance] = TOLERANCE_DEFAULT,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = MASK_AND_SCALE_FLAG_DEFAULT,
    in_memory: Annotated[bool, typer_option_in_memory] = IN_MEMORY_FLAG_DEFAULT,
    statistics: Annotated[bool, typer_option_statistics] = STATISTICS_FLAG_DEFAULT,
    groupby: Annotated[str | None, typer_option_groupby] = GROUPBY_DEFAULT,
    output_filename: Annotated[Path | None, typer_option_output_filename] = None,
    variable_name_as_suffix: Annotated[
        bool, typer_option_variable_name_as_suffix
    ] = True,
    rounding_places: Annotated[
        int | None, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = VERBOSE_LEVEL_DEFAULT,
):
    """Select location series"""
    if convert_longitude_360:
        longitude = longitude % 360
    warn_for_negative_longitude(longitude)

    if not variable:
        dataset = open_dataset(time_series)
        # ----------------------------------------------------- Review Me ----    
        #
        if len(dataset.data_vars) >= 2:
            variables = list(dataset.data_vars.keys())
            print(f"The dataset contains more than one variable : {variables}")
            variable = typer.prompt(
                "Please specify the variable you are interested in from the above list"
            )
        else:
            variable = list(dataset.data_vars)
        #
        # ----------------------------------------------------- Review Me ----    
    location_time_series = select_time_series(
        time_series=time_series,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        start_time=start_time,
        end_time=end_time,
        # convert_longitude_360=convert_longitude_360,
        variable=variable,
        coordinate=wavelength_column,
        minimum=min_wavelength,
        maximum=max_wavelength,
        drop=limit_spectral_range,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        in_memory=in_memory,
        # variable_name_as_suffix=variable_name_as_suffix,
        verbose=verbose,
        log=log,
    )
    location_time_series_2 = select_time_series(
        time_series=time_series_2,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        start_time=start_time,
        end_time=end_time,
        # convert_longitude_360=convert_longitude_360,
        variable=variable_2,
        coordinate=wavelength_column,
        minimum=min_wavelength,
        maximum=max_wavelength,
        drop=limit_spectral_range,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        in_memory=in_memory,
        # variable_name_as_suffix=variable_name_as_suffix,
        verbose=verbose,
        log=log,
    )
    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    results = {
        location_time_series.name: location_time_series.to_numpy(),
    }
    if location_time_series_2 is not None:
        more_results = {
            location_time_series_2.name: (
                location_time_series_2.to_numpy()
                if location_time_series_2 is not None
                else None
            )
        }
        results = results | more_results

    title = "Location time series"

    if verbose:
        # special case!
        if location_time_series is not None and timestamps is None:
            from pvgisprototype.cli.print.irradiance import print_irradiance_table_2

            timestamps = location_time_series.time.to_numpy()

        # if isinstance(location_time_series, DataArray):
        #     print_irradiance_xarray(
        #         location_time_series=location_time_series,
        #         longitude=longitude,
        #         latitude=latitude,
        #         # elevation=elevation,
        #         title=title,
        #         rounding_places=rounding_places,
        #         verbose=verbose,
        #         # index=index,
        #     )
        # else:
            print_irradiance_table_2(
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                dictionary=results,
                title=title,
                rounding_places=rounding_places,
                verbose=verbose,
            )

    # statistics after echoing series which might be Long!

    if statistics:
        from pvgisprototype.api.series.statistics import print_series_statistics

        print_series_statistics(
            data_array=location_time_series,
            timestamps=timestamps,
            groupby=groupby,
            title="Selected series",
            rounding_places=rounding_places,
        )

    # if csv:
        # from pvgisprototype.cli.write import export_statistics_to_csv
        # export_statistics_to_csv(
        #     data_array=location_time_series,
        #     filename=csv,
        # )

    output_handlers = {
        ".nc": lambda location_time_series, path: location_time_series.to_netcdf(path),
        ".csv": lambda location_time_series, path: to_csv(
            x=location_time_series, path=path
        ),
    }
    if output_filename:
        extension = output_filename.suffix.lower()
        if extension in output_handlers:
            output_handlers[extension](location_time_series, output_filename)
        else:
            raise ValueError(f"Unsupported file extension: {extension}")

warn_for_negative_longitude

warn_for_negative_longitude(longitude: Longitude = None)

Warn for negative longitude value

Maybe the input dataset ranges in [0, 360] degrees ?

Source code in pvgisprototype/cli/series/select.py
def warn_for_negative_longitude(
    longitude: Longitude = None,
):
    """Warn for negative longitude value

    Maybe the input dataset ranges in [0, 360] degrees ?
    """
    if longitude < 0:
        warning = f"{exclamation_mark} "
        warning += "The longitude "
        warning += f"{longitude} " + "is negative. "
        warning += "If the input dataset's longitude values range in [0, 360], consider using `--convert-longitude-360`!"
        logger.warning(warning)

write_to_netcdf

write_to_netcdf(
    location_time_series, path, longitude, latitude
)

Save to NetCDF with lon and lat dimensions of length 1.

Source code in pvgisprototype/cli/series/select.py
def write_to_netcdf(
    location_time_series,
    path,
    longitude,
    latitude,
):
    """Save to NetCDF with lon and lat dimensions of length 1."""
    # A new 'coords' for longitude and latitude, each of length 1
    new_coords = {
        'lon': numpy.array([longitude]),
        'lat': numpy.array([latitude]),
        'time': location_time_series.time
    }

    # Expand the data to include lat and lon dimensions
    new_data = location_time_series.values[:, numpy.newaxis, numpy.newaxis]

    # Create the new DataArray
    data_array = xarray.DataArray(
            new_data,
            coords=new_coords,
            dims=['time', 'lat', 'lon'],
            name=location_time_series.name,
            attrs=location_time_series.attrs,
    )

    # Add attributes to longitude and latitude for CF compliance
    data_array.lon.attrs['long_name'] = 'longitude'
    data_array.lon.attrs['units'] = 'degrees_east'
    data_array.lat.attrs['long_name'] = 'latitude'
    data_array.lat.attrs['units'] = 'degrees_north'

    # Save to NetCDF
    data_array.to_netcdf(path)

uniplot

Functions:

Name Description
uniplot

Plot time series in the terminal

uniplot

uniplot(
    time_series: Annotated[
        Path, typer_argument_time_series
    ],
    longitude: Annotated[
        float, typer_argument_longitude_in_degrees
    ],
    latitude: Annotated[
        float, typer_argument_latitude_in_degrees
    ],
    time_series_2: Annotated[
        Path | None, typer_option_time_series
    ] = None,
    timestamps: Annotated[
        DatetimeIndex | None, typer_argument_timestamps
    ] = None,
    start_time: Annotated[
        Timestamp | None, typer_option_start_time
    ] = None,
    end_time: Annotated[
        Timestamp | None, typer_option_end_time
    ] = None,
    variable: Annotated[
        str | None, typer_option_data_variable
    ] = None,
    coordinate: str | None = None,
    neighbor_lookup: Annotated[
        MethodForInexactMatches | None,
        typer_option_nearest_neighbor_lookup,
    ] = None,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = 0.1,
    mask_and_scale: Annotated[
        bool, typer_option_mask_and_scale
    ] = False,
    resample_large_series: Annotated[
        bool, "Resample large time series?"
    ] = False,
    lines: Annotated[
        bool, typer_option_uniplot_lines
    ] = True,
    title: Annotated[
        str | None, typer_option_uniplot_title
    ] = None,
    unit: Annotated[
        str, typer_option_uniplot_unit
    ] = UNIT_NAME,
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    data_source: Annotated[
        str,
        Option(
            help="Data source text to print in the footer of the plot."
        ),
    ] = "",
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
)

Plot time series in the terminal

Source code in pvgisprototype/cli/series/uniplot.py
def uniplot(
    time_series: Annotated[Path, typer_argument_time_series],
    longitude: Annotated[float, typer_argument_longitude_in_degrees],
    latitude: Annotated[float, typer_argument_latitude_in_degrees],
    time_series_2: Annotated[Path | None, typer_option_time_series] = None,
    timestamps: Annotated[DatetimeIndex | None, typer_argument_timestamps] = None,
    start_time: Annotated[Timestamp | None, typer_option_start_time] = None,
    end_time: Annotated[Timestamp | None, typer_option_end_time] = None,
    variable: Annotated[str | None, typer_option_data_variable] = None,
    coordinate: str | None = None,
    # convert_longitude_360: Annotated[bool, typer_option_convert_longitude_360] = False,
    neighbor_lookup: Annotated[
        MethodForInexactMatches | None, typer_option_nearest_neighbor_lookup
    ] = None,
    tolerance: Annotated[
        float | None, typer_option_tolerance
    ] = 0.1,  # Customize default if needed
    mask_and_scale: Annotated[bool, typer_option_mask_and_scale] = False,
    resample_large_series: Annotated[bool, "Resample large time series?"] = False,
    lines: Annotated[bool, typer_option_uniplot_lines] = True,
    title: Annotated[str | None, typer_option_uniplot_title] = None,
    unit: Annotated[str, typer_option_uniplot_unit] = UNIT_NAME,  # " °C")
    terminal_width_fraction: Annotated[
        float, typer_option_uniplot_terminal_width
    ] = TERMINAL_WIDTH_FRACTION,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    data_source: Annotated[str, typer.Option(help="Data source text to print in the footer of the plot.")] = '',
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
):
    """Plot time series in the terminal"""
    import os

    terminal_columns, _ = os.get_terminal_size()  # we don't need lines!
    terminal_length = int(terminal_columns * terminal_width_fraction)
    from functools import partial

    from uniplot import plot as default_plot

    plot = partial(default_plot, width=terminal_length)
    data_array = select_time_series(
        time_series=time_series,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        start_time=start_time,
        end_time=end_time,
        variable=variable,
        # convert_longitude_360=convert_longitude_360,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        # in_memory=in_memory,
        verbose=verbose,
    )
    if resample_large_series:
        data_array = data_array.resample(time="1M").mean()
    data_array_2 = select_time_series(
        time_series=time_series_2,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        start_time=start_time,
        end_time=end_time,
        variable=variable,
        # convert_longitude_360=convert_longitude_360,
        neighbor_lookup=neighbor_lookup,
        tolerance=tolerance,
        mask_and_scale=mask_and_scale,
        # in_memory=in_memory,
        verbose=verbose,
    )
    if resample_large_series:
        data_array_2 = data_array_2.resample(time="1M").mean()

    if isinstance(data_array, float):
        print(
            f"⚠️{exclamation_mark} [red]Aborting[/red] as I [red]cannot[/red] plot the single float value {float}!"
        )
        typer.Abort()

    if not isinstance(data_array, DataArray):
        print("Selected variable did not return a DataArray. Check your selection.")
        return

    if isinstance(data_array, DataArray):
        supertitle = getattr(data_array, "long_name", "Untitled")
        label = getattr(data_array, "name", None)
        label_2 = (
            getattr(data_array_2, "name", None)
            if isinstance(data_array_2, DataArray)
            else None
        )
        data_source_text = ''
        if data_source:
            data_source_text = f" · {data_source}"

        if fingerprint:
            from pvgisprototype.core.hashing import generate_hash
            data_source_text += f" · Fingerprint : {generate_hash(data_array)}"

        if label_2:
            label_2 += data_source_text

        else:
            label += data_source_text

        unit = getattr(data_array, "units", None)
        if coordinate in data_array.coords:
            plot(
                # x=data_array,
                xs=data_array[coordinate],
                ys=[data_array, data_array_2] if data_array_2 is not None else data_array,
                legend_labels=[label, label_2],
                lines=lines,
                title=title if title else supertitle,
                x_unit=' ' + getattr(data_array[coordinate], 'units', ''),
                y_unit=' ' + str(unit),
                # force_ascii=True,
            )
        else:
            # plot over time
            plot(
                # x=data_array,
                # xs=data_array,
                ys=[data_array, data_array_2] if data_array_2 is not None else data_array,
                legend_labels=[label, label_2],
                lines=lines,
                title=title if title else supertitle,
                y_unit=" " + str(unit),
                # force_ascii=True,
            )

surface

Modules:

Name Description
elevation
horizon

elevation

Functions:

Name Description
get_elevation

Retrieve the location elevation from digital elevation data

get_elevation

get_elevation(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
)

Retrieve the location elevation from digital elevation data

Args: longitude latitude

Notes: - Based on the original C program readelevation: - Variable, Type, Range, Default, Notes - lat, float, [-90, 90], -, Required - lon, float, [-180, 180], -, Required

Source code in pvgisprototype/cli/surface/elevation.py
def get_elevation(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
):
    """
    Retrieve the location elevation from digital elevation data

    Args:
        longitude
        latitude

    Notes:
        - Based on the original C program `readelevation`:
        - Variable, Type, Range, Default, Notes
        - lat, float, [-90, 90], -, Required
        - lon, float, [-180, 180], -, Required
    """
    pass

horizon

Functions:

Name Description
get_horizon

Calculate the entire horizon angle height (in radians) around a single point from a digital elevation model

get_horizon

get_horizon(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
)

Calculate the entire horizon angle height (in radians) around a single point from a digital elevation model

Notes: - Based on the original C program horizon_out - Variable, Typr, Range, Default, Notes - lat, float, [-90, 90], -, Latitude in decimal degrees, south is negative. Required - lon, float, [-180, 180], - , Longitude in decimal degrees, west is negative. Required - userhorizon, list, List of float values ranging in [0, 90] separated by comma (CSV) (length < =365), -, Height of the horizon at equidistant directions around the point of interest, in degrees. Starting at north and moving clockwise. The series 0, 10, 20, 30, 40, 15, 25, 5 would mean the horizon height is 0° due north, 10° for north-east, 20° for east, 30° for south-east, and so on. Optional, Depends on userhorizon=1, - outputformat, str, [csv, basic, json], csv, Output format. csv: CSV with text explanations, basic: CSV. Optional - browser, bool, 0, 1, 0, Setting browser=1 and accessing the service through a web browser, will save the retrieved data to a file. Optional

Source code in pvgisprototype/cli/surface/horizon.py
def get_horizon(
    longitude: Annotated[float, typer_argument_longitude],
    latitude: Annotated[float, typer_argument_latitude],
):
    """Calculate the entire horizon angle height (in radians) around a single point from a digital elevation model

    Notes:
        - Based on the original C program `horizon_out`
        - Variable, Typr, Range, Default, Notes
        - lat, float, [-90, 90], -, Latitude in decimal degrees, south is negative. Required
        - lon, float, [-180, 180], - , Longitude in decimal degrees, west is negative. Required
        - userhorizon, list, List of float values ranging in [0, 90] separated by comma (CSV) (length < =365), -, Height of the horizon at equidistant directions around the point of interest, in degrees.
          Starting at north and moving clockwise. The series 0, 10, 20, 30, 40,
          15, 25, 5 would mean the horizon height is 0° due north, 10° for
          north-east, 20° for east, 30° for south-east, and so on. Optional,
          Depends on `userhorizon=1`,
        - outputformat, str, [csv, basic, json], csv, Output format. csv: CSV with text explanations, basic: CSV. Optional
        - browser, bool, 0, 1, 0, Setting browser=1 and accessing the service through a web browser, will save the retrieved data to a file. Optional
    """
    pass

time

Important sun and solar surface geometry parameters in calculating the amount of solar radiation that reaches a particular location on the Earth's surface

Functions:

Name Description
correction
equation_of_time
fractional_year
local
offset
solar_time

Calculate the solar time.

correction

correction()
Source code in pvgisprototype/cli/time.py
@app.command(
    "correction",
    no_args_is_help=True,
    help=f"{SYMBOL_TIMING}{SYMBOL_FACTOR} Calculate the time correction {NOT_IMPLEMENTED_CLI}",
)
def correction():
    """ """
    pass

equation_of_time

equation_of_time()
Source code in pvgisprototype/cli/time.py
@app.command(
    "eot",
    no_args_is_help=True,
    help=f"={SYMBOL_TIMING} Calculate the equation of time {NOT_IMPLEMENTED_CLI}",
)
def equation_of_time():
    """ """
    pass

fractional_year

fractional_year()
Source code in pvgisprototype/cli/time.py
@app.command(
    "fractional-year",
    no_args_is_help=True,
    help=f"{SYMBOL_TIMESTAMP}/ Calculate the fractional year {NOT_IMPLEMENTED_CLI}",
)
def fractional_year():
    """ """
    pass

local

local()
Source code in pvgisprototype/cli/time.py
@app.command(
    "local",
    no_args_is_help=True,
    help=f"{SYMBOL_TIME} Calculate the local time {NOT_IMPLEMENTED_CLI}",
)
def local():
    """ """
    pass

offset

offset()
Source code in pvgisprototype/cli/time.py
@app.command(
    "offset",
    no_args_is_help=True,
    help=f"{SYMBOL_PLUS_MINUS} Calculate the time offset {NOT_IMPLEMENTED_CLI}",
)
def offset():
    """ """
    pass

solar_time

solar_time(
    longitude: Annotated[float, typer_argument_longitude],
    timestamps: Annotated[
        DatetimeIndex, typer_argument_timestamps
    ] = str(now_utc_datetimezone()),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,
    timezone: Annotated[
        str | None, typer_option_timezone
    ] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,
    solar_time_model: Annotated[
        List[SolarTimeModel], typer_option_solar_time_model
    ] = [milne],
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    csv: Annotated[
        Path, typer_option_csv
    ] = CSV_PATH_DEFAULT,
    dtype: Annotated[
        str, typer_option_dtype
    ] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[
        str, typer_option_array_backend
    ] = ARRAY_BACKEND_DEFAULT,
    verbose: Annotated[
        int, typer_option_verbose
    ] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[
        int, typer_option_log
    ] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[
        bool, typer_option_fingerprint
    ] = FINGERPRINT_FLAG_DEFAULT,
    index: Annotated[
        bool, typer_option_index
    ] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[
        bool, typer_option_quiet
    ] = QUIET_FLAG_DEFAULT,
)

Calculate the solar time.

  1. Map the day of the year onto the circumference of a circle, essentially converting the day of the year into radians.

  2. Approximate empirically the equation of time, which accounts for the elliptical shape of Earth's orbit and the tilt of its axis.

  3. Calculate the solar time by adding the current hour of the year, the time offset from the equation of time, and the hour offset (likely a longitude-based correction).

Source code in pvgisprototype/cli/time.py
@app.command(
    "solar",
    no_args_is_help=True,
    help=f"{SYMBOL_HOUR_ANGLE} Calculate the apparent solar time",
)
def solar_time(
    longitude: Annotated[float, typer_argument_longitude],
    timestamps: Annotated[DatetimeIndex, typer_argument_timestamps] = str(
        now_utc_datetimezone()
    ),
    start_time: Annotated[
        datetime | None, typer_option_start_time
    ] = None,  # Used by a callback function
    periods: Annotated[
        int | None, typer_option_periods
    ] = None,  # Used by a callback function
    frequency: Annotated[
        str | None, typer_option_frequency
    ] = None,  # Used by a callback function
    end_time: Annotated[
        datetime | None, typer_option_end_time
    ] = None,  # Used by a callback function
    timezone: Annotated[str | None, typer_option_timezone] = None,
    random_timestamps: Annotated[
        bool, typer_option_random_timestamps
    ] = RANDOM_TIMESTAMPS_FLAG_DEFAULT,  # Used by a callback function
    solar_time_model: Annotated[List[SolarTimeModel], typer_option_solar_time_model] = [
        SolarTimeModel.milne
    ],
    angle_output_units: Annotated[
        str, typer_option_angle_output_units
    ] = ANGLE_OUTPUT_UNITS_DEFAULT,
    rounding_places: Annotated[
        int, typer_option_rounding_places
    ] = ROUNDING_PLACES_DEFAULT,
    csv: Annotated[Path, typer_option_csv] = CSV_PATH_DEFAULT,
    dtype: Annotated[str, typer_option_dtype] = DATA_TYPE_DEFAULT,
    array_backend: Annotated[str, typer_option_array_backend] = ARRAY_BACKEND_DEFAULT,
    verbose: Annotated[int, typer_option_verbose] = VERBOSE_LEVEL_DEFAULT,
    log: Annotated[int, typer_option_log] = LOG_LEVEL_DEFAULT,
    fingerprint: Annotated[bool, typer_option_fingerprint] = FINGERPRINT_FLAG_DEFAULT,
    index: Annotated[bool, typer_option_index] = INDEX_IN_TABLE_OUTPUT_FLAG_DEFAULT,
    quiet: Annotated[bool, typer_option_quiet] = QUIET_FLAG_DEFAULT,
):
    """Calculate the solar time.

    1. Map the day of the year onto the circumference of a circle, essentially
    converting the day of the year into radians.

    2. Approximate empirically the equation of time, which accounts for the
    elliptical shape of Earth's orbit and the tilt of its axis.

    3. Calculate the solar time by adding the current hour of the year, the
    time offset from the equation of time, and the hour offset (likely a
    longitude-based correction).
    """
    # Note the input timestamp and timezone
    user_requested_timestamps = timestamps
    user_requested_timezone = timezone  # Set to UTC by the callback functon !
    timezone = utc_zoneinfo = ZoneInfo("UTC")
    logger.debug(
        f"Input time zone : {timezone}",
        alt=f"Input time zone : [code]{timezone}[/code]",
    )

    if timestamps.tz is None:
        timestamps = timestamps.tz_localize(utc_zoneinfo)
        logger.debug(
            f"Naive input timestamps\n({user_requested_timestamps})\nlocalized to UTC aware for all internal calculations :\n{timestamps}"
        )

    elif timestamps.tz != utc_zoneinfo:
        timestamps = timestamps.tz_convert(utc_zoneinfo)
        logger.debug(
            f"Input zone\n{user_requested_timezone}\n& timestamps :\n{user_requested_timestamps}\n\nconverted for all internal calculations to :\n{timestamps}",
            alt=f"Input zone : [code]{user_requested_timezone}[/code]\n& timestamps :\n{user_requested_timestamps}\n\nconverted for all internal calculations to :\n{timestamps}",
        )

    solar_time_models = select_models(
        SolarTimeModel, solar_time_model
    )  # Using a callback fails!

    solar_time_series = calculate_solar_time_series(
        longitude=longitude,
        timestamps=timestamps,
        timezone=timezone,
        solar_time_models=solar_time_models,
        dtype=dtype,
        array_backend=array_backend,
        verbose=verbose,
        log=log,
    )
    longitude = convert_float_to_degrees_if_requested(longitude, angle_output_units)

    if not quiet:
        from pvgisprototype.cli.print.solar_time import print_solar_time_series_table

        print_solar_time_series_table(
            longitude=longitude,
            timestamps=timestamps,
            timezone=timezone,
            solar_time_series=solar_time_series,
            title="Solar Time Overview",
            rounding_places=rounding_places,
            index=index,
        )
    if csv:
        pass

write

Functions:

Name Description
collect_leaf_columns

Recursively collect leaf keys and their values from nested dict.

create_csv_export_panel

Create a Rich Panel displaying CSV export information.

flatten_dict_for_csv

Recursively flatten nested OrderedDicts, producing

print_csv_export_info

Print a formatted info panel for CSV export.

safe_get_value

Parameters

write_irradiance_csv

Write time series data to a CSV file in a structured format along with

write_metadata
write_solar_position_series_csv

Write the "output" of solar position overview data to a CSV file.

write_spectral_factor_csv

Write the spectral factor data to a CSV file.

write_surface_position_csv

collect_leaf_columns

collect_leaf_columns(d, out=None)

Recursively collect leaf keys and their values from nested dict. Return dict of leaf_key -> value.

Source code in pvgisprototype/cli/write.py
def collect_leaf_columns(d, out=None):
    """
    Recursively collect leaf keys and their values from nested dict.
    Return dict of leaf_key -> value.
    """
    if out is None:
        out = {}
    for key, val in d.items():
        if isinstance(val, (dict, OrderedDict)):
            collect_leaf_columns(val, out)
        else:
            # leaf key only, no prefix
            out[key] = val
    return out

create_csv_export_panel

create_csv_export_panel(
    filename: Path, num_rows: int, num_columns: int
) -> Panel

Create a Rich Panel displaying CSV export information.

Parameters:

Name Type Description Default
filename Path

Output CSV file path

required
num_rows int

Number of data rows written

required
num_columns int

Number of columns in CSV

required
location_info str

Location/coordinate information

required
time_range_info str

Time range information

required

Returns:

Type Description
Panel

Rich Panel with formatted export information

Source code in pvgisprototype/cli/write.py
def create_csv_export_panel(
    filename: Path,
    num_rows: int,
    num_columns: int,
) -> Panel:
    """
    Create a Rich Panel displaying CSV export information.

    Parameters
    ----------
    filename : Path
        Output CSV file path
    num_rows : int
        Number of data rows written
    num_columns : int
        Number of columns in CSV
    location_info : str, optional
        Location/coordinate information
    time_range_info : str, optional
        Time range information

    Returns
    -------
    Panel
        Rich Panel with formatted export information
    """
    # Create info table
    info_table = Table(box=None, show_header=False, padding=(0, 1))
    info_table.add_column(style="cyan", width=20)
    info_table.add_column(style="white")

    # Add rows with icons and info
    info_table.add_row("📁 File", Text(str(filename), style="bold green"))
    info_table.add_row("_ Rows", Text(str(num_rows), style="bold yellow"))
    info_table.add_row("| Columns", Text(str(num_columns), style="bold blue"))

    panel = Panel(
        info_table,
        title="[bold]CSV output[/bold]",
        title_align="left",
        border_style="dim",
        box=ROUNDED,
        padding=(1, 2),
        expand=False,
    )

    return panel

flatten_dict_for_csv

flatten_dict_for_csv(d, out=None)

Recursively flatten nested OrderedDicts, producing keys as tuples and values as scalars, arrays, or lists.

Source code in pvgisprototype/cli/write.py
def flatten_dict_for_csv(d, out=None):
    """
    Recursively flatten nested OrderedDicts, producing
    keys as tuples and values as scalars, arrays, or lists.
    """
    if out is None:
        out = {}
    for k, v in d.items():
        if isinstance(v, dict) or isinstance(v, OrderedDict):
            flatten_dict_for_csv(v, out)
        else:
            # Use only the leaf key (column name), not the full path
            out[str(k)] = v
    return out

print_csv_export_info

print_csv_export_info(
    filename: Path, num_rows: int, num_columns: int
) -> None

Print a formatted info panel for CSV export.

Parameters:

Name Type Description Default
filename Path

Output CSV file path

required
num_rows int

Number of data rows written

required
num_columns int

Number of columns in CSV

required
Source code in pvgisprototype/cli/write.py
def print_csv_export_info(
    filename: Path,
    num_rows: int,
    num_columns: int,
) -> None:
    """
    Print a formatted info panel for CSV export.

    Parameters
    ----------
    filename : Path
        Output CSV file path
    num_rows : int
        Number of data rows written
    num_columns : int
        Number of columns in CSV
    """
    from rich.console import Console

    console = Console()
    panel = create_csv_export_panel(
        filename=filename,
        num_rows=num_rows,
        num_columns=num_columns,
    )
    console.print(panel)

safe_get_value

safe_get_value(
    dictionary, key, index, default=NOT_AVAILABLE
)

Parameters:

Name Type Description Default
dictionary

Input dictionary

required
key

key to retrieve from the dictionary

required
index

index ... ?

required

Returns:

Type Description
The value corresponding to the given `key` in the `dictionary` or the
default value if the key does not exist.
Source code in pvgisprototype/cli/write.py
def safe_get_value(dictionary, key, index, default=NOT_AVAILABLE):
    """
    Parameters
    ----------
    dictionary: dict
        Input dictionary
    key: str
        key to retrieve from the dictionary
    index: int
        index ... ?

    Returns
    -------
    The value corresponding to the given `key` in the `dictionary` or the
    default value if the key does not exist.

    """
    value = dictionary.get(key, default)
    # if isinstance(value, ndarray) and value.size > 1:
    if isinstance(value, (list, ndarray)) and len(value) > index:
        return value[index]
    return value

write_irradiance_csv

write_irradiance_csv(
    longitude=None,
    latitude=None,
    timestamps=None,
    dictionary=None,
    index=False,
    filename=Path("irradiance.csv"),
)

Write time series data to a CSV file in a structured format along with non-time-series scalar metadata to a separate [JSON | YAML] file.

This function takes location information (longitude and latitude), a time series (timestamps), and a (nested) dictionary containing irradiance or photovoltaic power data, generated by PVGIS' API functions, and writes them into a CSV file.

Attention ! The function modifies the input dictionary by removing certain keys (such as 'Title' and 'Fingerprint') to avoid repeated values in the output file. It is essential to place this function last in the workflow if the original dictionary is needed elsewhere in the code, as it alters the input dictionary in place to save memory.

Parameters:

Name Type Description Default
longitude float

Longitude of the location to include in the CSV output.

None
latitude float

Latitude of the location to include in the CSV output.

None
timestamps list

A Pandas DatetimeIndex. Each timestamp will correspond to a row in the CSV file.

None
dictionary dict

A dictionary containing the irradiance or photovoltaic data, where each key is a variable name and each value is either a list/array of data or a single value to be replicated for all timestamps.

None
index bool

If True, an index column will be added to the CSV file, where each row will be numbered sequentially.

False
filename Path

The output file path where the CSV will be saved. Defaults to "irradiance.csv".

Path('irradiance.csv')
Notes
  • Attention : this function is optimized to avoid deep copying the dictionary, reducing memory consumption. It should be placed at the end of any process that requires the original dictionary to remain unmodified !

  • Fingerprint information is removed from the input dictionary and added as part of the filename.

  • Single float or integer values in the dictionary are expanded to match the length of the timestamps.

  • This function expects the photovoltaic_power_output_series.components structure to match the format required for writing to CSV.

Example

write_irradiance_csv( longitude=-3.7038, latitude=40.4168, timestamps=some_timestamps, dictionary=some_data, filename=Path("output.csv"), index=True )

This will generate a CSV file named 'output.csv' with the specified data and location.

Source code in pvgisprototype/cli/write.py
def write_irradiance_csv(
    longitude=None,
    latitude=None,
    timestamps=None,
    dictionary=None,
    index=False,
    filename=Path("irradiance.csv"),
):
    """
    Write time series data to a CSV file in a structured format along with
    non-time-series scalar metadata to a separate [JSON | YAML] file.

    This function takes location information (longitude and latitude), a time
    series (timestamps), and a (nested) dictionary containing irradiance or
    photovoltaic power data, generated by PVGIS' API functions, and writes them
    into a CSV file.

    Attention ! The function modifies the input dictionary by removing certain
    keys (such as 'Title' and 'Fingerprint') to avoid repeated values in the
    output file. It is essential to place this function last in the workflow if
    the original dictionary is needed elsewhere in the code, as it alters the
    input dictionary in place to save memory.

    Parameters
    ----------
    longitude : float, optional
        Longitude of the location to include in the CSV output.

    latitude : float, optional
        Latitude of the location to include in the CSV output.

    timestamps : list, optional
        A Pandas DatetimeIndex. Each timestamp will correspond to a row in the
        CSV file.

    dictionary : dict
        A dictionary containing the irradiance or photovoltaic data, where 
        each key is a variable name and each value is either a list/array of 
        data or a single value to be replicated for all timestamps.

    index : bool, optional
        If True, an index column will be added to the CSV file, where each 
        row will be numbered sequentially.

    filename : Path, optional
        The output file path where the CSV will be saved. Defaults to 
        "irradiance.csv".

    Notes
    -----
    - Attention : this function is optimized to avoid deep copying the
      dictionary, reducing memory consumption. It should be placed _at the end_
      of any process that requires the original dictionary to remain unmodified
      !

    - Fingerprint information is removed from the input dictionary and added 
      as part of the filename.

    - Single float or integer values in the dictionary are expanded to match 
      the length of the timestamps.

    - This function expects the `photovoltaic_power_output_series.components` 
      structure to match the format required for writing to CSV.

    Example
    -------
    >>> write_irradiance_csv(
            longitude=-3.7038,
            latitude=40.4168,
            timestamps=some_timestamps,
            dictionary=some_data,
            filename=Path("output.csv"),
            index=True
        )

    This will generate a CSV file named 'output.csv' with the specified 
    data and location.

    """
    if dictionary is None or timestamps is None:
        raise ValueError("Both dictionary and timestamps must be provided.")

    filename = Path(filename)

    leaf_columns = collect_leaf_columns(dictionary)

    time_series_columns = []
    metadata = {}

    for column_name, val in leaf_columns.items():
        # Identify time series arrays matching the timestamps length
        if isinstance(val, (np.ndarray, list)) and len(val) == len(timestamps):
            time_series_columns.append((column_name, val))
        else:
            metadata[column_name] = val

    header = []
    if index:
        header.append("Index")

    if longitude is not None:
        header.append("Longitude")

    if latitude is not None:
        header.append("Latitude")

    header.append("Time")
    header.extend([col for col, _ in time_series_columns])

    rows = []
    for idx, time in enumerate(timestamps):
        row = []

        if index:
            row.append(idx)

        if longitude is not None:
            row.append(longitude)

        if latitude is not None:
            row.append(latitude)

        row.append(time.strftime("%Y-%m-%d %H:%M:%S"))
        for _, array in time_series_columns:
            value = array[idx] if array is not None and len(array) > idx else ""

            if isinstance(value, (float, np.floating)):
                value = round_float_values(float(value), 4)

            row.append(value)
        rows.append(row)

    # Write to CSV
    fingerprint = retrieve_fingerprint(dictionary)

    # Add me with a flag ? ------------------------------------------
    # if not fingerprint:
    #     fingerprint = Timestamp.now().isoformat(timespec="seconds")
    #     # Sanitize the ISO datetime for a safe filename
    # ---------------------------------------------------------------

    if fingerprint:
        safe_fingerprint = re.sub(r"[:]", "-", fingerprint)  # Replace colons with hyphens
        safe_fingerprint = safe_fingerprint.replace(" ", "T")  # Ensure ISO format with 'T'
        filename = filename.with_stem(filename.stem + f"_{safe_fingerprint}")

    with filename.open("w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(header)
        writer.writerows(rows)

    write_metadata(
        metadata=metadata,
        filename=filename,
        formats=("yaml"),
    )

    print_csv_export_info(
        filename=filename,
        num_rows=len(rows),
        num_columns=len(header),
    )

write_metadata

write_metadata(
    metadata, filename, formats=("json", "yaml", "txt")
)
Source code in pvgisprototype/cli/write.py
def write_metadata(
    metadata,
    filename,
    formats=("json", "yaml", "txt"),
):
    """
    """
    metadata_filename = filename.with_name(filename.stem + "_metadata")
    if "json" in formats:
        import json
        with open(Path(f"{metadata_filename}.json"), "w") as f:
            json.dump(metadata, f, indent=2, default=str)

    if "yaml" in formats:
        from pvgisprototype.core.hashing import convert_numpy_to_json_serializable
        safe_metadata = convert_numpy_to_json_serializable(metadata)
        import yaml
        with open(Path(f"{metadata_filename}.yaml"), "w") as f:
            yaml.safe_dump(safe_metadata, f, sort_keys=False, allow_unicode=True)

    if "txt" in formats:
        with open(Path(f"{metadata_filename}.txt"), "w") as f:
            for k, v in metadata.items():
                f.write(f"{k}: {v}\n")

write_solar_position_series_csv

write_solar_position_series_csv(
    longitude: float,
    latitude: float,
    timestamps,
    timezone: str,
    table: dict,
    position_parameters: Sequence[SolarPositionParameter],
    index: bool = False,
    rounding_places: int = 2,
    filename: Path = Path("solar_position_overview.csv"),
) -> None

Write the "output" of solar position overview data to a CSV file.

This function flattens the nested table dictionary and writes time-series data as CSV, handling numpy arrays, enums, and special types.

Source code in pvgisprototype/cli/write.py
def write_solar_position_series_csv(
    longitude: float,
    latitude: float,
    timestamps,
    timezone: str,
    table: dict,
    position_parameters: Sequence[SolarPositionParameter],
    index: bool = False,
    rounding_places: int = 2,
    filename: Path = Path("solar_position_overview.csv"),
) -> None:
    """
    Write the "output" of solar position overview data to a CSV file.

    This function flattens the nested table dictionary and writes time-series
    data as CSV, handling numpy arrays, enums, and special types.

    """
    import csv
    import re
    from numpy import datetime64, isnat, bool_
    from pandas import to_datetime, isna
    import numpy

    def find_nested_value(d: dict, key: str):
        """
        Helper function to find nested values
        """
        if key in d:
            return d[key]
        for v in d.values():
            if isinstance(v, dict):
                found = find_nested_value(v, key)
                if found is not None:
                    return found
        return None

    def value_in_dict(value, d):
        """Check if value is in dictionary by object identity."""
        for v in d.values():
            if v is value:
                return True
        return False

    def get_scalar(value_array, idx, rounding_places):
        """
        Helper to safely get a scalar value from an array at a specific index
        """
        if value_array is None:
            return None
        if not hasattr(value_array, "__len__"):
            return value_array
        if len(value_array) <= idx:
            return None

        value = value_array[idx]

        # Round numeric values
        if isinstance(value, (int, float, numpy.floating, numpy.integer)):
            return round(float(value), rounding_places)

        return value

    # Extract the first model result (e.g., 'noaa')
    first_model_key = next(iter(table))
    model_result = table[first_model_key]

    # Extract core data and events data
    core_data = model_result.get("Core", {})
    events_data = model_result.get("Solar Events", {})
    algorithms_data = model_result.get("Solar Position Algorithms", {})

    # Build header columns
    header = []
    columns_to_extract = []  # Store (header_name, data_source_dict) tuples

    if index:
        header.append("Index")

    header.extend(["Longitude", "Latitude", "Time", "Timezone"])

    # Add columns for each requested parameter
    for parameter in position_parameters:
        # Skip enum members without a matching ColumnName
        if parameter.name not in SolarPositionParameterColumnName.__members__:
            continue

        # Get the human-readable column name
        column_name = SolarPositionParameterColumnName[parameter.name].value

        # Find where this data lives
        value = None
        source_dict = None

        if column_name in core_data:
            value = core_data[column_name]
            source_dict = core_data
        elif column_name in events_data:
            value = events_data[column_name]
            source_dict = events_data
        elif column_name in algorithms_data:
            value = algorithms_data[column_name]
            source_dict = algorithms_data
        else:
            # Try nested search
            value = find_nested_value(model_result, column_name)
            if value is not None:
                # Use identity check instead of 'in' to avoid array comparison
                if value_in_dict(value, core_data):
                    source_dict = core_data
                elif value_in_dict(value, events_data):
                    source_dict = events_data
                elif value_in_dict(value, algorithms_data):
                    source_dict = algorithms_data

        if value is None:
            continue

        # For event columns, check if there's actual data
        if parameter in (
            SolarPositionParameter.event_type,
            SolarPositionParameter.event_time,
        ):
            if not hasattr(value, "__iter__") or isinstance(value, str):
                value_list = [value]
            else:
                value_list = value

            def is_real_event(ev):
                if isinstance(ev, datetime64):
                    return not isnat(ev)
                if ev is not None and hasattr(ev, "name") and ev.name == "none":
                    return False
                return ev not in (None, "None")

            has_data = any(is_real_event(v) for v in value_list)
            if not has_data:
                continue

        # Clean column name for CSV (remove special characters)
        clean_column_name = re.sub(r"[^A-Za-z0-9 ]+", "", column_name).strip()
        header.append(clean_column_name)
        columns_to_extract.append((column_name, source_dict))

    # Build rows
    rows = []
    for idx, timestamp in enumerate(timestamps):
        row = []

        if index:
            row.append(str(idx))

        # Add location and time info
        row.append(str(longitude))
        row.append(str(latitude))
        row.append(to_datetime(timestamp).strftime("%Y-%m-%d %H:%M:%S"))
        row.append(str(timezone))

        # Extract each parameter value for this timestamp
        for column_name, source_dict in columns_to_extract:
            if source_dict is None:
                row.append("")
                continue

            value_array = source_dict.get(column_name)
            value = get_scalar(value_array, idx, rounding_places)

            # Format value for CSV
            if value is None or (isinstance(value, float) and isna(value)):
                row.append("")
            elif isinstance(value, SolarEvent):
                row.append(value.value)
            elif isinstance(value, datetime64):
                if isnat(value):
                    row.append("")
                else:
                    dt = value.astype("datetime64[s]").astype("O")
                    row.append(str(dt.time()))
            elif isinstance(value, (bool_, bool)):
                row.append(str(bool(value)))
            elif isinstance(value, (int, float, numpy.generic)):
                row.append(str(value))
            else:
                row.append(str(value))

        rows.append(row)

    # Write to CSV
    with open(filename, "w", newline="") as file:
        writer = csv.writer(file)
        writer.writerow(header)
        writer.writerows(rows)

    print_csv_export_info(
        filename=filename,
        num_rows=len(rows),
        num_columns=len(header),
    )

write_spectral_factor_csv

write_spectral_factor_csv(
    longitude,
    latitude,
    timestamps: DatetimeIndex,
    spectral_factor_dictionary: Dict,
    filename: Path = Path("spectral_factor.csv"),
    index: bool = False,
)

Write the spectral factor data to a CSV file.

Source code in pvgisprototype/cli/write.py
def write_spectral_factor_csv(
    longitude,
    latitude,
    timestamps: DatetimeIndex,
    spectral_factor_dictionary: Dict,
    filename: Path = Path("spectral_factor.csv"),
    index: bool = False,
):
    """
    Write the spectral factor data to a CSV file.

    Parameters
    ----------
    - longitude: Longitude of the location.
    - latitude: Latitude of the location.
    - timestamps: DatetimeIndex of the time series.
    - spectral_factor_dictionary: Dictionary containing spectral factor data.
    - filename: Path for the output CSV file.
    - index: Whether to include the index in the CSV.

    """
    header = []
    if index:
        header.append("Index")
    if longitude:
        header.append("Longitude")
    if latitude:
        header.append("Latitude")

    header.append("Time")

    # Prepare the data for each spectral factor model and module type
    data_rows = []
    for spectral_factor_model, result in spectral_factor_dictionary.items():
        for module_type, data in result.items():
            spectral_factor_series = data.get(SPECTRAL_FACTOR_COLUMN_NAME)

            # If spectral_factor_series is a scalar, expand it to match the length of timestamps
            if isinstance(spectral_factor_series, (float, int)):
                spectral_factor_series = full(len(timestamps), spectral_factor_series)

            # Add the header for this particular module type and spectral factor model
            header.append(f"{module_type.value} ({spectral_factor_model.name})")

            # Prepare the rows
            for idx, timestamp in enumerate(timestamps):
                if len(data_rows) <= idx:
                    data_row = []
                    if index:
                        data_row.append(idx)
                    if longitude and latitude:
                        data_row.extend([longitude, latitude])
                    data_row.append(timestamp.strftime("%Y-%m-%d %H:%M:%S"))
                    data_rows.append(data_row)

                # Append spectral factor data for this timestamp and module type
                data_rows[idx].append(spectral_factor_series[idx])

    # Write to CSV
    with filename.open("w", newline="") as file:
        writer = csv.writer(file)
        writer.writerow(header)  # Write header
        writer.writerows(data_rows)  # Write rows of data

write_surface_position_csv

write_surface_position_csv(
    longitude: float,
    latitude: float,
    timestamps: list = [],
    timezone: str | None = None,
    dictionary: dict = {},
    fingerprint: str | None = None,
    index: bool = False,
    filename: Path = Path("optimal_surface_position.csv"),
) -> None
Source code in pvgisprototype/cli/write.py
def write_surface_position_csv(
    longitude: float,
    latitude: float,
    timestamps: list = [],
    timezone: str | None = None,
    dictionary: dict = {},
    fingerprint: str | None = None,
    index: bool = False,
    filename: Path = Path("optimal_surface_position.csv"),
) -> None:
    """
    """
    # remove 'Title' and 'Fingerprint' : we don't want repeated values ! ----
    dictionary.pop("Title", NOT_AVAILABLE)
    fingerprint = dictionary.pop(FINGERPRINT_COLUMN_NAME, None)
    if not fingerprint:
        fingerprint = Timestamp.now().isoformat(timespec="seconds")
        # Sanitize the ISO datetime for a safe filename
    safe_fingerprint = re.sub(r"[:]", "-", fingerprint)  # Replace colons with hyphens
    safe_fingerprint = safe_fingerprint.replace(" ", "T")  # Ensure ISO format with 'T'
    # ------------------------------------------------------------- Important

    header: list = []
    if index:
        header.insert(0, "Index")
    if longitude:
        header.append("Longitude")
    if latitude:
        header.append("Latitude")

    header.append("Start Time")
    header.append("End Time")
    header.append("Timezone")
    header.extend(dictionary.keys())


    # Convert special types to strings
    for key, value in dictionary.items():
        if isinstance(value, generic):  # NumPy scalar
            dictionary[key] = str(value)
        elif (isinstance(value, SurfaceOrientation) or isinstance(value, SurfaceTilt)) and hasattr(value, "value"):  # Enums or custom objects with .value
            dictionary[key] = str(value.value)
        elif isinstance(value, (float, int)):
            dictionary[key] = str(value)

    # Compose single row
    row = []
    if index:
        row.append(0)
    row.extend([
        longitude, # type:ignore[list-item]
        latitude, # type:ignore[list-item]
        timestamps[0].strftime("%Y-%m-%d %H:%M:%S"),
        timestamps[-1].strftime("%Y-%m-%d %H:%M:%S"),
        timezone, # type:ignore[list-item]
    ])
    row.extend(dictionary.values())

    rows = [row]

    # Generate filename with fingerprint
    if fingerprint:
        filename = filename.with_stem(filename.stem + f"_{safe_fingerprint}")

    # Write to CSV
    with filename.open("w", newline="") as file:
        writer = csv.writer(file)
        writer.writerow(header)
        writer.writerows(rows)